Skip to content
创建时间: 2020年10月8日

结构类型系统

1. 接口的兼容性

  • 如果传入的变量和声明的类型不匹配,TS就会进行兼容性检查
  • 原理是Duck-Check,就是说只要目标类型中声明的属性变量在源类型中都存在就是兼容的
ts
interface Animal {
  name: string;
  age: number;
}

interface Person {
  name: string;
  age: number;
  gender: number;
}
// 要判断目标类型`Person`是否能够兼容输入的源类型`Animal`
function getName(animal: Animal): string {
  return animal.name;
}

let p = {
  name: "hello ",
  age: 10,
  gender: 0,
};

getName(p);
//只有在传参的时候两个变量之间才会进行兼容性的比较,赋值的时候并不会比较,会直接报错
let a: Animal = {
  name: "hello ",
  age: 10,
  gender: 0,
};

2. 基本类型的兼容性

ts
//基本数据类型也有兼容性判断
let num: string | number;
let str: string = "hello ";
num = str;

//只要有toString()方法就可以赋给字符串变量
let num2: {
  toString(): string;
};

let str2: string = "jiagou";
num2 = str2;

3. 类的兼容性

  • 在TS中是结构类型系统,只会对比结构而不在意类型
ts
class Animal {
  name: string;
}
class Bird extends Animal {
  swing: number;
}

let a: Animal;
a = new Bird();

let b: Bird;
//并不是父类兼容子类,子类不兼容父类
b = new Animal();
ts
class Animal {
  name: string;
}
//如果父类和子类结构一样,也可以的
class Bird extends Animal {}

let a: Animal;
a = new Bird();

let b: Bird;
b = new Animal();
ts
//甚至没有关系的两个类的实例也是可以的
class Animal {
  name: string;
}
class Bird {
  name: string;
}
let a: Animal;
a = new Bird();
let b: Bird;
b = new Animal();

4. 函数的兼容性

  • 比较函数的时候是要先比较函数的参数,再比较函数的返回值

4.1 比较参数

ts
type sumFunc = (a: number, b: number) => number;
let sum: sumFunc;
function f1(a: number, b: number): number {
  return a + b;
}
sum = f1;

//可以省略一个参数
function f2(a: number): number {
  return a;
}
sum = f2;

//可以省略二个参数
function f3(): number {
  return 0;
}
sum = f3;

//多一个参数可不行
function f4(a: number, b: number, c: number) {
  return a + b + c;
}
sum = f4;

4.2 比较返回值

ts
type GetPerson = () => { name: string; age: number };
let getPerson: GetPerson;
//返回值一样可以
function g1() {
  return { name: "hello ", age: 10 };
}
getPerson = g1;
//返回值多一个属性也可以
function g2() {
  return { name: "hello ", age: 10, gender: "male" };
}
getPerson = g2;
//返回值少一个属性可不行
function g3() {
  return { name: "hello " };
}
getPerson = g3;
//因为有可能要调用返回值上的方法
getPerson().age.toFixed();

5. 函数的协变与逆变

  • 协变(Covariant):只在同一个方向;

  • 逆变(Contravariant):只在相反的方向;

  • 双向协变(Bivariant):包括同一个方向和不同方向;

  • 不变(Invariant):如果类型不完全相同,则它们是不兼容的。

  • A ≼ B 意味着 A 是 B 的子类型。

  • A → B 指的是以 A 为参数类型,以 B 为返回值类型的函数类型。

  • x : A 意味着 x 的类型为 A

  • 返回值类型是协变的,而参数类型是逆变的

  • 返回值类型可以传子类,参数可以传父类

  • 参数逆变父类 返回值协变子类 搀你父,返鞋子

ts
class Animal {}
class Dog extends Animal {
  public name: string = "Dog";
}
class BlackDog extends Dog {
  public age: number = 10;
}
class WhiteDog extends Dog {
  public home: string = "北京";
}
let animal: Animal;
let blackDog: BlackDog;
let whiteDog: WhiteDog;
type Callback = (dog: Dog) => Dog;
function exec(callback: Callback): void {
  callback(whiteDog);
}
//不行  callback(redDog);
type ChildToChild = (blackDog: BlackDog) => BlackDog;
const childToChild: ChildToChild = (blackDog: BlackDog): BlackDog => blackDog;
exec(childToChild);

//也不行,理由同上
type ChildToParent = (blackDog: BlackDog) => Animal;
const childToParent: ChildToParent = (blackDog: BlackDog): Animal => animal;
exec(childToParent);

//不行 因为有可能调用返回的Dog的方法
type ParentToParent = (animal: Animal) => Animal;
const parentToParent: ParentToParent = (animal: Animal): Animal => animal;
exec(parentToParent);

//可以,所有的狗都是动物,返回的不管什么狗都是狗
type ParentToChild = (animal: Animal) => BlackDog;
const parentToChild: ParentToChild = (animal: Animal): BlackDog => blackDog;
exec(parentToChild);
//(Animal → Greyhound) ≼ (Dog → Dog)
//返回值类型很容易理解:黑狗是狗的子类。但参数类型则是相反的:动物是狗的父类!
ts
// string | number|boolean 是 string | number的父类型
// string是string|number的子类型
type Callback2 = (a: string | number) => string | number;
function exec2(callback: Callback2): void {
  callback("");
}
type ParentToChild2 = (a: string | number | boolean) => string;
const parentToChild2: ParentToChild2 = (a: string | number | boolean): string =>
  "";
exec2(parentToChild2);

type Callback3 = (a: string | number) => string | number;
function exec3(callback: Callback2): void {
  callback("");
}
type ParentToParent3 = (a: string) => string;
const parentToParent3: ParentToParent3 = (a: string): string => "";
exec3(parentToChild3);
  • TypeScript 中, 参数类型是双向协变的 ,也就是说既是协变又是逆变的,而这并不安全。但是现在你可以在 TypeScript 2.6 版本中通过 --strictFunctionTypes--strict 标记来修复这个问题

6. 泛型的兼容性

  • 泛型在判断兼容性的时候会先判断具体的类型,然后再进行兼容性判断
ts
//接口内容为空没用到泛型的时候是可以的
//1.接口内容为空没用到泛型的时候是可以的
interface Empty<T> {}
let x!: Empty<string>;
let y!: Empty<number>;
x = y;

//2.接口内容不为空的时候不可以
interface NotEmpty<T> {
  data: T;
}
let x1!: NotEmpty<string>;
let y1!: NotEmpty<number>;
x1 = y1;

//实现原理如下,称判断具体的类型再判断兼容性
interface NotEmptyString {
  data: string;
}

interface NotEmptyNumber {
  data: number;
}
let xx2!: NotEmptyString;
let yy2!: NotEmptyNumber;
xx2 = yy2;

7. 枚举的兼容性

  • 枚举类型与数字类型兼容,并且数字类型与枚举类型兼容
  • 不同枚举类型之间是不兼容的
ts
//数字可以赋给枚举
enum Colors {
  Red,
  Yellow,
}
let c: Colors;
c = Colors.Red;
c = 1;
c = "1";

//枚举值可以赋给数字
let n: number;
n = 1;
n = Colors.Red;