TypeScript学习日记《二》类型兼容性(协变、逆变、双变和抗变)
类型父子关系的判断
像 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 | interface Person { name: string; age: number; } |
逆变:printY = printX 函数X 类型可以赋值给函数Y 类型,因为函数Y 在调用的时候参数是按照Y类型进行约束的,但是用到的是函数X的X的属性和方法,ts检查结果是类型安全的。这种特性就叫做逆变,函数的参数有逆变的性质(而返回值是协变的,也就是子类型可以赋值给父类型)。
1 | let printY: (man: Man) => void |
双变(双向协变):x = y
;y = x
父类型的值可以赋值给子类型的值,子类型的值可以赋值给父类型的值,既逆变又协变,叫做“双向协变”(ts2.x 之前支持这种赋值,之后 ts 加了一个编译选项 strictFunctionTypes
,设置为 true
就只支持函数参数的逆变,设置为 false
则支持双向协变);
抗变(不变):非父子类型之间不会发生型变,只要类型不一样就会报错。
总结
ts 通过给 js 添加了静态类型系统来保证了类型安全,大多数情况下不同类型之间是不能赋值的,但是为了增加类型系统灵活性,设计了父子类型的概念。父子类型之间自然应该能赋值,也就是会发生型变。
型变分为逆变和协变。协变很容易理解,就是子类型赋值给父类型。逆变主要是函数赋值的时候函数参数的性质,参数的父类型可以赋值给子类型,这是因为按照子类型来声明的参数,访问父类型的属性和方法自然没问题,依然是类型安全的。但反过来就不一定了。
不过 ts 2.x 之前反过来依然是可以赋值的,也就是既逆变又协变,叫做双向协变。
为了更严格的保证类型安全,ts 添加了 strictFunctionTypes
的编译选项,开启以后(设置为true)函数参数就只支持逆变,否则支持双向协变。
型变都是针对父子类型来说的,非父子类型自然就不会型变也就是不变。
ts 中父子类型的判定是按照结构来看的,更具体的那个是子类型。
理解了如何判断父子类型(结构类型系统),父子类型的型变(逆变、协变、双向协变),很多类型兼容问题就能得到解释了。