我也是最近刚接触到了这些知识,文章可能有些错误,希望大佬多多指点(
对于学习 TypeScript 了解类型的逆变、协变、双向协变和不变是很重要的,但你只要明白类型的父子级关系,这些概念理解起来就会容易许多,因此在讲述这些之前我们必须先学会类型的父子级关系。
类型的父子级
首先明确一个概念,对于 TypeScript 而言,只要类型结构上是一致的,那么就可以确定父子关系,这点与 Java 是不一样的(Java 必须通过 extends 才算继承)。
我们可以看下面的例子:
typescript
你应该可以发现这两个类型是有继承关系,此时你可以去思考到底谁是父级、谁是子级?
你可能会觉得 Suemor 是 Person 的父类型(毕竟 Person 有 2 个属性,而 Suemor 有 3 个属性且包含 Person),如果是这么理解的话那就错。
在类型系统中,属性更多的类型是子类型,也就是说 Suemor 是 Person 的子类型。
因为这是反直觉的,你可能很难理解(我当时也理解不了),你可以尝试这样去理解:因为 A extends B , 于是 A 就可以去扩展 B 的属性,那么 A 的属性往往会比 B 更多,因此 A 就是子类型。或者你记住一个特征,子类型比父类型更加具体。
另外判断联合类型父子关系的时候, 'a' | 'b' 和 'a' | 'b' | 'c' 哪个更具体?
'a' | 'b' 更具体,所以 'a' | 'b' 是 'a' | 'b' | 'c' 的子类型。
协变
对象中运用
协变理解起来很简单,你可能在平日里开发经常用到,例如:
typescript
这俩类型不一样,但是 suemor 却可以赋值给 person,也就是子级可以赋值给父级,反之不行(至于为什么,你可以想想假如 person 能够正确赋值给 suemor,那么调用 suemor.hobbies你的程序就坏到了)。
因此得出结论: 子类型可以赋值给父类型的情况就叫做协变。
函数中运用
同样的函数中也可以用到协变,例如:
typescript
这里我们多给一个 hobbies,同理因为协变,子类型可以赋值给父类型。
因此我们平日的redux,在声明 dispatch 类型的时候,可以这样去写:
typescript
这样约束了传入的参数一定是 Action 的子类型,也就是说必须有 type,其他的属性有没有都可以。
双向协变
我们再看一下上上节的例子:
typescript
suemor = person的报错我们可以在 tsconfig.json设置 strictFunctionTypes:false或者关闭严格模式,此时我们父类型可以赋值给子类型,子类型可以赋值给父类型,这种情况我们便称为双向协变。
因此双向协变就是: 父类型可以赋值给子类型,子类型可以赋值给父类型。
但是这明显是有问题的,不能保证类型安全,因此我们一般都会打开严格模式,避免出现双向协变。
不变
不变是最简单的。如果没有继承关系(A 和 B 没有一方包含对方全部属性)那它就是不变,因此非父子类型之间只要类型不一样就会报错:
typescript
逆变
逆变相对难理解一点,看下方例子:
typescript
你会发现:fn1 的参数是 fn2 的参数的父类型,那为啥能赋值给子类型?
这就是逆变,父类型可以赋值给子类型,函数的参数有逆变的性质(而返回值是协变的,也就是子类型可以赋值给父类型)。
至于为什么,如果fn1 = fn2是正确的话,我们只能传入fn1('suemor',123),但 fn1调却要输出 c,那就坏掉了。
因此我感觉逆变一般会出现在: 父函数参数与子函数参数之间赋值的时候(注意是函数与函数之间,而不是调用函数的时候,我是这么理解的,不知道对不对)。
因为逆变相对在类型做运算时用的会多一点,因此我们再看一个稍微难一点例子:
typescript
image-20230203205737963这里GetReturnType使用来提取返回值类型,这里ReturnTypeResullt原本应当是suemor,但如上代码却得出结果为never。
因为函数参数遵循逆变,也就是只能父类型赋值给子类型,但很明显这里的 unknown 是 {name: string} 的父类型,所以反了,应该把unknown改为string的子类型才行,所以应该把 unknown 改为any或者never,如下为正确答案:
typescript
image-20230203205711934