-
Notifications
You must be signed in to change notification settings - Fork 250
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
TypeScript 中的子类型、逆变、协变是什么? #54
Comments
棒👏🏻 |
有一个问题,在《深入理解typescript》这本书说到函数“返回值类型是协变的,而参数类型是逆变的”,怎么理解“返回值类型是协变的”? |
非常感谢,看了一堆文章,在你的基础上瞎说了一堆,还好文末附了您的链接,讲的很通透! |
收到了。~辛苦了亲
|
已收到阁下的来件 我会尽早回复的
|
例子直观,终于理解逆变是啥了 |
已收到阁下的来件 我会尽早回复的
|
举个例子 let returnAnimal = ():Animal => {
return animal;
}
let returnDog = ():Dog => {
return dog;
} 因为 至于为什么合法,因为 其实总结一下返回值类型协变+参数类型逆变,两者不同是因为考虑安全性的 block 不同:
|
已收到阁下的来件 我会尽早回复的
|
描述一个对象 属性越多越具体 |
收到了。~辛苦了亲
|
总结的很通透👏 函数类型兼容,我就记住了「参逆反协」规则 这两篇文章说的也挺好的 |
收到了。~辛苦了亲
|
前言
TypeScript 中有很多地方涉及到子类型
subtype
、父类型supertype
、逆变和协变covariance and contravariance
的概念,如果搞不清这些概念,那么很可能被报错搞的无从下手,或者在写一些复杂类型的时候看到别人可以这么写,但是不知道为什么他可以生效。(就是我自己没错了)子类型
比如考虑如下接口:
在这个例子中,
Animal
是Dog
的父类,Dog
是Animal
的子类型,子类型的属性比父类型更多,更具体。也就是说,子类型是父类型的超集,而父类型是子类型的子集,这是直觉上容易搞混的一点。
记住一个特征,子类型比父类型更加具体,这点很关键。
可赋值性
assignable
assignable
是类型系统中很重要的一个概念,当你把一个变量赋值给另一个变量时,就要检查这两个变量的类型之间是否可以相互赋值。从这个例子里可以看出,
animal
是一个「更宽泛」的类型,它的属性比较少,所以更「具体」的子类型是可以赋值给它的,因为你是知道animal
上只有age
这个属性的,你只会去使用这个属性,dog
上拥有animal
所拥有的一切类型,赋值给animal
是不会出现类型安全问题的。反之,如果
dog = animal
,那么后续使用者会期望dog
上拥有bark
属性,当他调用了dog.bark()
就会引发运行时的崩溃。从可赋值性角度来说,子类型是可以赋值给父类型的,也就是
父类型变量 = 子类型变量
是安全的,因为子类型上涵盖了父类型所拥有的的一切属性。当我初学的时候,我会觉得
T extends {}
这样的语句很奇怪,为什么可以extends
一个空类型并且在传递任意类型时都成立呢?当搞明白上面的知识点,这个问题也自然迎刃而解了。在函数中的运用
假设我们有这样的一个函数:
有这样两个变量:
调用
f(val1)
是会报错的,比较显而易见的来看是因为缺少属性b
,而函数f
中很可能去访问b
属性并且做一些操作,比如b.substr()
,这就会导致崩溃。换成上面的知识点来看,
val1
对应的类型是{ a: number }
,它是{ a: number, b: number }
的父类型,调用f(val1)
其实就相当于把函数定义中的形参val
赋值成了val1
,把父类型的变量赋值给子类型的变量,这是危险的。
反之,调用
f(val2)
没有任何问题,因为val2
的类型是val
类型的子类型,它拥有更多的属性,函数有可能使用的一切属性它都有。假设我现在要开发一个
redux
,在声明dispatch
类型的时候,我就可以这样去做:这样,就约束了传入的参数一定是
Action
的子类型。也就是说,必须有type
,其他的属性有没有,您随意。在联合类型中的运用
学习了以上知识点,再看联合类型的可赋值性,乍一看会比较反直觉,
'a' | 'b' | 'c'
是'a' | 'b'
的子类型吗?它看起来属性更多诶?其实正相反,'a' | 'b' | 'c'
是'a' | 'b'
的父类型。因为前者比后者更「宽泛」,后者比前者更「具体」。这里
son
是可以安全的赋值给parent
的,因为son
的所有可能性都被parent
涵盖了。而反之则不行,
parent
太宽泛了,它有可能是'c'
,这是Son
类型 hold 不住的。这个例子看完以后,你应该可以理解为什么
'a' | 'b' extends 'a' | 'b' | 'c'
为 true 了,在书写conditional types
的时候更加灵活的运用吧。逆变和协变
先来段维基百科的定义:
描述的比较晦涩难懂,但是用我们上面的动物类型的例子来解释一波,现在我们还是有
Animal
和Dog
两个父子类型。协变(Covariance)
那么想象一下,现在我们分别有这两个子类型的数组,他们之间的父子关系应该是怎么样的呢?没错,
Animal[]
依然是Dog[]
的父类型,对于这样的一段代码,把子类型赋值给父类型依然是安全的:转变成数组之后,对于父类型的变量,我们依然只会去
Dog
类型中一定有的那些属性。那么,对于
type MakeArray<T> = T[]
这个类型构造器来说,它就是协变(Covariance)
的。逆变(Contravariance)
有这样两个函数:
animal = dog
是类型安全的,那么visitAnimal = visitDog
好像也是可行的?其实不然,想象一下这两个函数的实现:由于
visitDog
的参数期望的是一个更具体的带有bark
属性的子类型,所以如果visitAnimal = visitDog
后,我们可能会用一个不带bark
属性的普通的animal
类型来传给visitDog
。这会造成运行时错误,
animal.bark
根本不存在,去调用这个方法会引发崩溃。但是反过来,
visitDog = visitAnimal
却是完全可行的。因为后续调用方会传入一个比animal
属性更具体的dog
,函数体内部的一切访问都是安全的。在对
Animal
和Dog
类型分别调用如下的类型构造器之后:父子类型关系逆转了,这就是
逆变(Contravariance)
。在 TS 中
当然,在 TypeScript 中,由于灵活性等权衡,对于函数参数默认的处理是
双向协变
的。也就是既可以visitAnimal = visitDog
,也可以visitDog = visitAnimal
。在开启了tsconfig
中的strictFunctionType
后才会严格按照逆变
来约束赋值关系。结语
这篇文章结合我自己最近学习类型相关知识的一些心得整理而成,如果有错误或者疏漏欢迎大家指出。
参考资料
Subsets & Subtypes
TypeScript 官方文档
维基百科-协变与逆变
The text was updated successfully, but these errors were encountered: