typescript中的协变、逆变
想要写出更加优秀的类型编程co-variance(协变)contra-variance(逆变)这类知识是我们必须掌握的。这篇记录也仅仅是为了方便之后哪天这块知识在应用过程中出现问题后,能够根据此篇,来快速定位编写代码中的问题。这篇也仅仅是一些学习资料的记录。文中许多表述,都是个人的理解毕竟本人学历较低英语水平较差,资料中的表述多有理解不充分的地方。 虽然我有些过许多文章,但这篇是我在掘金的第一篇。我所写的文章大多是进行一些学习后的知识梳理。因为我没有什么水平来进行一些底层的源码分析,我写下文章正如上面所述只是为了在不熟练时方便查错。菜鸟专科生在此请多指教。
首先我们需要知道以下的概念。在泛型编程中泛型参数会影响类型泛型的关系。
出于ts
采用的是结构类型系统,这里我们用 subtype
来表示来表示可分配的关系。泛型参数引起的协变、逆变都是设计者出于类型安全性的问题而设计的。
以下的例子我们都以Dog
、Animal
为基本类型,根据类型关系Dog extends Animal
进行分析
定义中以List<T>
来阐述关系
定义
co-variance定义
在Dog is subtype of Animal
的情况下,List<Dog> is subtypeof List<Animal>
我们称这种List<T>
泛型变化为协变
contra-variance定义
在Dog is subtype of Animal
的情况下,List<Animal> is subtypeof List<Dog>
我们称这种List<T>
泛型变化为逆变
in-variance定义(这里的我们可以理解为独立类型)
在Dog is subtype of Animal
的情况下,List<Animal> 与 List<Dog>
没有什么关系
bi-variance定义(能够相互赋值)
在Dog is subtype of Animal
的情况下,List<Animal> 与 List<Dog>
能够相互赋值
type List<T> = T[]
const animals:List<Animal> = [] as List<Animal>
const dogs:List<Dog> = animals //no error
const animals2: List<Animal> = dogs //no error
做完这些定义我们可以看几个例子以此来搞明白这么做有什么意义。
例子
//base types
class Animal {
public weight = 0;
}
class Dog extends Animal {
public isGoodBoy = true;
public bark() {
console.log("wofi");
}
}
class Cat extends Animal {
public isPlotting: "yes" | "no" = "yes";
public play() {
console.error("miau");
}
}
例子1
interface Cage<T> {
readonly animal: T;
}
let dogCage: Cage<Dog> = { animal: new Dog() };
let cage: Cage<Animal> = dogCage; //这里类型安全因为Animal是关系链的最高级,所有与之有关的对象都是对其进行扩展
let catCage: Cage<Cat> = cage; //这里cage实际已经被之前的操作换成了dogCage,并且ts编译器已检查出了错误
catCage.animal.play(); //假设我们让编译通过,catCage中存在的其实是dog对象,不存在play方法,因此类型不安全
例1说明readonly
的field
引起泛型关系的co-variance
例子2
interface Mother<T> {
create: () => T;
}
let dogMother: Mother<Dog> = {
create() {
return new Dog();
},
};
let animationMother: Mother<Animal> = dogMother;//success
let catMother: Mother<Cat> = animationMother; //error:'Mother<Animal>' is not assignable to type 'Mother<Cat>'
catMother.create().play(); //与例1相同狗狗被当成猫对待类型不安全=>return type是co-variance
例2说明function signature(函数签名)
方法的return type
引起泛型关系的contra-variance
例子3
//例3
interface Groomer<T> {
cuthair: (animal: T) => void;
}
let dogGroomer: Groomer<Dog> = {
cuthair(dog) {
dog.bark(); //这里调用了子类独有的操作狗叫
},
};
let animalGroomer: Groomer<Animal> = dogGroomer; //Error: 因为狗狗是对animal的拓展,animal中不存在bark方法
animalGroomer.cuthair(new Animal()); //这里假设能进行编译,运行时将会有 dog.bark is not a function报错
let animalGroomer2: Groomer<Animal> = {
cuthair(animal) {
console.log(animal.weight); //所有的动物都具有的weight,所以猫猫也可以cuthair
},
};
let catGroomer: Groomer<Cat> = animalGroomer2; //当方法参数的变化引起contra-variance
例子3说明function signature(函数签名)
方法的参数位引起泛型关系的`contra-variance
得出一个普遍结论
- 1.由方法实现产出的为co-variance
- 2.参与到方法实现中的为cotra-variance
补充
针对函数参数位,我在上面写下结论的时候有带上function signature(函数签名)
的字样
那什么是方法签名呢?
interface lll {
methods(): void; //method signature
functions: () => void; //function signature
}
为了使这两种表示产生类型编程时的不同,我们需要--strictFunctiontypes
,这是针对于函数参数位
的检查function signature
会采用更加严格的方式
strictFunctiontype
只会影响function signatures
,所以method signature是bi-variance,例如:lib.es5.ts库中类似Array其中的method signature不受到strictFunctiontype
影响- contra-variance - when used in function signatures with strictFunctiontype(使用function signature引起逆变)
- bi-variance - when used in function signatures without strictFunctiontype
- bi-variance - when used in method signatures
为什么在参数位置的variance这么复杂?
为了js的用户群里能更快的迁移是其中的一点,针对老练的玩家可以使用strictFunctiontype
编写出更加优秀的类型代码,而针对初级玩家就可以规避这些编译问题。
类型编程建议
-
可以更多地使用readonly
-
更多地使用function signatures
-
在接口中考虑variance
- 优先 co / contra variance
- 其次使用invariance
- 尽可能少的bi-variance
bi-variance虽然不会给你带来很多红色的波浪线但是类型编程带来的程序维护性的收益也就更低。
比如将例子3进行更改
//例3
interface Groomer<T> {
cuthair(animal: T): void; //这里从原来的function signature改为method signature
}
let dogGroomer: Groomer<Dog> = {
cuthair(dog) {
dog.bark(); //这里调用了子类独有的操作狗叫
},
};
let animalGroomer: Groomer<Animal> = dogGroomer;
animalGroomer.cuthair(new Animal()); //这里已经不会有类型检查的报错了,但是运行时将会有 dog.bark is not a function报错
\