废话
今天深入的看了下TypeScript兼容性,陷入了迷茫,看了多个文章更迷茫了,最后只能根据自己查的资料,最后花了三小时总算搞懂了!!!!总结就是别人的文章过于文字化了或者没有具象化,给我产生了歧义,盯着 A类型变成B类型是协变还是逆变了,但是关键是想表达安全性,我手撸一下原因来讲讲
如果还没有看过官方文档(虽然很少很少内容)可以先看一下,大概有个概念
类型兼容性 · TypeScript中文网 · TypeScript——JavaScript的超集 (tslang.cn)
1 结构化和兼容
正常看别人文章都有子集、父集之类的,我觉得没必要,占用脑子注意力。
首先我们要明确的是TypeScrip是一个结构类型兼容,意思就是,只要你给我东西里面有我想要的,我就兼容你。
懂?
不懂:我要养一只狗,你却有猫和狗,那么我和你就很适合,看一下伪代码:
我:{
狗
}
你:{
狗,
猫
}
我=你 // ts允许兼容,因为你有狗
你=我 // ts不允许兼容,因为我没有猫
正常代码定义:
interface Animal { name: string };
interface Dog extends Animal { myIsDog: string };
let animal: Animal = { name: '动物' };
let dog: Dog = { name: '狗', myIsDog: '我是狗' };
animal = dog;// dog包含了animal的属性, animal兼容dog
dog = animal;// 反之对animal进行dog的操作,animal不一定有,不安全 ts报错
总结:只要赋值给我的数据结构,有我要的所有的属性,就是兼容的
2 协变 Covariant
其实
animal = dog //也算协变
或者加深一点
type Co<T> = T[];
let adimalCo: Covariant<Animal> = [];
let dogCo: Covariant<Dog> = [];
adimalCo = dogCo;
协变没什么好讲的,就是看完官方文档,知道什么是兼容就行。
总结:把子类型赋值给父类型(狗(Dog)属于动物(Animal))我们定义为这个行为是安全的,与这个标准相反的基本就是逆变的意思。
3 逆变 Contravariant
然后我们看下代码
interface Animal { name: string };
interface Dog extends Animal { myIsDog: string };
interface TuDog extends Dog { myIsTuDog: string };
interface Cat extends Animal { myIsTuCat: string };
let animal: Animal = { name: '动物' };
let dog: Dog = { name: '狗', myIsDog: '我是狗' };
let tuDog: TuDog = { name: '土狗', myIsDog: '我是狗', myIsTuDog: '我是土狗' };
let cat: Cat = { name: '猫', myIsTuCat: '我是猫' };
let isDog = function (p: Dog): Dog {
console.log(p.name)
console.log(p.myIsDog);
return p as TuDog
}
let isAnimal = function (p: Animal): Dog {
console.log(p.name)
return p as TuDog
}
isDog = isAnimal
最后isDog实际变成了isAnimal方法了,最后一句isDog = isAnimal后类型解析isDog还是原本的类型:let isDog: (p: Dog) => Dog,也就是说我调用isDog函数的时候,参数可以传Dog类型的。
可是我们限制了isAnimal方法参数是p:Animal,那么我们在方法内部的一切操作,是基于Animal有的属性去操作的,传入Dog,Dog有Animal有的所有东西,所以很安全。我们在这里相当于用参数Dog类型执行了参数Animal类型的函数。
isAnimal = isDog为什么不行 不安全呢?看代码:
isAnimal = isDog;
/** 等于
isAnimal = function (p: Dog): Dog {
console.log(p.name)
console.log(p.myIsDog);
return p as TuDog
}
**/
animal = cat;
isAnimal(animal); // 执行的时候cat.myIsDog 没有这个属性
4 Bivariant 双向协变
其实这是一个问题,Ts官方的妥协,第三点说到isAnimal = isDog 不安全,但是如果isDog里面的操作也只操作了Animal有的属性,那其实就没有问题,所以官方给出了一个配置在tsconfig.json:strictFunctionType:true,设置为true 编辑器会报错,设置为false就不会 这样就可以互相转换,具体举例子像代码这种情况:
interface Animal { name: string };
interface Dog extends Animal { myIsDog: string };
interface TuDog extends Dog { myIsTuDog: string };
interface Cat extends Animal { myIsTuCat: string };
let animal: Animal = { name: '动物' };
let dog: Dog = { name: '狗', myIsDog: '我是狗' };
let tuDog: TuDog = { name: '土狗', myIsDog: '我是狗', myIsTuDog: '我是土狗' };
let cat: Cat = { name: '猫', myIsTuCat: '我是猫' };
let animals: Animal[] = []
let addAnimal = function (p: Animal) {
animals.push(p)
}
let addDog = function addDog(p: Dog) {
animals.push(p)
}
addDog = addAnimal
addAnimal = addDog
我们想要知道总共有多少动物,可以把所有动物集中起来,然后再计算数组长度不就好了,其实不会有什么安全性问题
5 Invariant 不可变
很好理解既然可以双向协变(互相变),肯定有相反的,两者都不可以互相赋值的情况。
interface Animal { name: string };
interface Dog extends Animal { myIsDog: string };
interface TuDog extends Dog { myIsTuDog: string };
interface Cat extends Animal { myIsTuCat: string };
let animal: Animal = { name: '动物' };
let dog: Dog = { name: '狗', myIsDog: '我是狗' };
let tuDog: TuDog = { name: '土狗', myIsDog: '我是狗', myIsTuDog: '我是土狗' };
let cat: Cat = { name: '猫', myIsTuCat: '我是猫' };
let dogFun = function (p: Dog): Dog {
console.log(p.myIsDog)
return {
name: p.name,
myIsDog: p.myIsDog
}
}
let tuDogFun = function (p: TuDog): TuDog {
console.log(p.myIsDog)
console.log(p.myIsTuDog)
return {
name: p.name,
myIsDog: p.myIsDog,
myIsTuDog: p.myIsTuDog
}
}
// 相当于在tuDogFun方法内调用了 dog.myIsTuDog 不安全
dog = tuDog
dogFun = tuDogFun
dogFun(dog)
let tuDogFun2 = function (p: TuDog): TuDog {
console.log(p.myIsDog)
console.log(p.myIsTuDog)
return {
name: p.name,
myIsDog: p.myIsDog,
myIsTuDog: p.myIsTuDog
}
}
// 如果可以把dogFun给tuDogFun 实际返回的是一只狗,但是我们tuDogFun的类型(p: TuDog) => TuDog是土狗,tuDog2的类型被推断成了TuDog,
// 执行tuDogFun2的时候其实是没有myIsTuDog属性的 -- 不安全
tuDogFun = dogFun
let tuDog2 = tuDogFun(tuDog)
tuDogFun2(tuDog2)
// 相当于把Dog当成TuDog用 ts直接就不允许了
tuDog = dog