类型父子关系的判断

像 java 里面的类型都是通过 extends 继承的,如果 A extends B,那 A 就是 B 的子类型。这种叫做名义类型系统(nominal type)

而 ts 里不看这个,只要结构上是一致的,那么就可以确定父子关系,这种叫做结构类型系统(structual type)

TypeScript结构化类型系统的基本规则是,如果x要兼容y,那么y至少具有与x相同的属性。
ts 类型兼容:
当一个类型 Y 的值可以赋值给另一个类型 X 的值时, 我们就可以说类型 X 兼容类型 Y。也就是说两者在结构上是一致的,而不一定非得通过 extends 的方式继承而来;
接口的兼容性x = y 只要目标类型 X 的值x中声明的属性变量在源类型 Y 的值y中都存在就是兼容的( Y 中的类型可以比 X 中的多,但是不能少);
函数的兼容性x = y Y 的每个参数必须能在 X 里找到对应类型的参数,参数的名字相同与否无所谓,只看它们的类型(参数可以少但是不能多。与接口的兼容性有区别)。

支持子类对象赋值给父类对象的情况称之为协变;反之,支持父类对象赋值给子类对象的情况称之为逆变。(注意:这里所说的子类不一定是通过继承关系的子类,而是类型结构上的相同)。

协变X = Y Y 类型可以赋值给 X 类型的情况就叫做协变,也可以说是 X 类型兼容 Y 类型;

1
2
3
4
5
6
interface Person { name: string; age: number; } 
interface Man { name: string; age: number; hobbies: string[] }
let person: Person = { name: 'xiaoming', age: 16 }
let man: Man = { name: 'xiaohong', age: 18, hobbies: ['eat'] }
person = man // OK
man = person // Error

逆变:printY = printX 函数X 类型可以赋值给函数Y 类型,因为函数Y 在调用的时候参数是按照Y类型进行约束的,但是用到的是函数X的X的属性和方法,ts检查结果是类型安全的。这种特性就叫做逆变,函数的参数有逆变的性质(而返回值是协变的,也就是子类型可以赋值给父类型)

1
2
3
4
5
6
let printY: (man: Man) => void
printY = (man) => { console.log(man.hobbies) }
let printX: (p: Person) => void
printX = (p) => { console.log(p.name) }
printY = printX // OK
printX = printY // Error

双变(双向协变)x = yy = x父类型的值可以赋值给子类型的值,子类型的值可以赋值给父类型的值,既逆变又协变,叫做“双向协变”(ts2.x 之前支持这种赋值,之后 ts 加了一个编译选项 strictFunctionTypes,设置为 true 就只支持函数参数的逆变,设置为 false 则支持双向协变);

抗变(不变):非父子类型之间不会发生型变,只要类型不一样就会报错。

总结

ts 通过给 js 添加了静态类型系统来保证了类型安全,大多数情况下不同类型之间是不能赋值的,但是为了增加类型系统灵活性,设计了父子类型的概念。父子类型之间自然应该能赋值,也就是会发生型变。

型变分为逆变协变协变很容易理解,就是子类型赋值给父类型逆变主要是函数赋值的时候函数参数的性质,参数的父类型可以赋值给子类型,这是因为按照子类型来声明的参数,访问父类型的属性和方法自然没问题,依然是类型安全的。但反过来就不一定了。

不过 ts 2.x 之前反过来依然是可以赋值的,也就是既逆变又协变,叫做双向协变。

为了更严格的保证类型安全,ts 添加了 strictFunctionTypes 的编译选项,开启以后(设置为true)函数参数就只支持逆变,否则支持双向协变。

型变都是针对父子类型来说的,非父子类型自然就不会型变也就是不变。

ts 中父子类型的判定是按照结构来看的,更具体的那个是子类型。

理解了如何判断父子类型(结构类型系统),父子类型的型变(逆变、协变、双向协变),很多类型兼容问题就能得到解释了。