当前位置: 首页 > 科技观察

快速理解TypeScript的倒置和协变

时间:2023-03-21 01:19:44 科技观察

如果深入研究TypeScript类型系统,倒置、协变、双向协变和不变性是绕不开的概念。这些概念看似很高大上,其实并不复杂。让我们在本文中学习它们。类型安全和类型改变TypeScript在JavaScript中加入了静态类型系统来保证类型安全,即保证变量只能被赋值相同类型,对象只能访问它的属性和方法。例如,number类型的值不能赋值给boolean类型的变量,Date类型的对象不能调用exec方法。这就是类型检查的作用。当遇到类型安全问题时,编译时会报错。但是,这种类型安全限制不应该太死板。有时需要一些灵活性。例如,一个子类型可以赋值给父类型的一个变量,并且可以作为父类型使用,即“类型改变”(typechange)。这种“类型改变”分为两种,一种是子类型可以赋给父类型,称为协变,另一种是父类型可以赋给子类型,称为逆变.我们先来看看协方差:协方差很好理解。例如,我们有两个接口:interfacePerson{name:string;年龄:数字;}界面广{姓名:字符串;年龄:数字;hobbies:string[]}在??这里,Guang是Person的子类型。更具体一点,可以将一个类型为Guang的变量赋值给一个类型为Person:这样就不会报错了。尽管这两种类型不同,但它们仍然是类型安全的。这种可以将子类型分配给超类型的情况称为协方差。为什么支持协变很好理解:类型系统支持父子类型,那么如果子类型不能赋值给父类型,还叫父子类型吗?因此,类型改变是实现类型父子关系的必要条件,它保证了类型安全。基于此,增加了类型系统的灵活性。倒排比较难理解:我们有两个倒排的函数:Person)=>void;printName=(person)=>{console.log(person.name);}printHobbies的参数是printName参数的子类型。那么问题来了,printName可以赋给printHobbies吗?printHobbies可以分配给printName吗?经过测试发现是这样的:printName的参数不就是printHobbies的父类型吗?为什么它可以分配给一个子类型?因为这个函数在调用的时候是按照Guang进行约束的类型,但是实际上这个函数只是使用了父类型Person的属性和方法,当然没有问题,而且还是类型安全的。这是逆变。函数的参数具有逆变性(且返回值是协变的,即子类型可以赋值给父类型)。反过来,如果将printHoobies分配给printName会发生什么?因为函数声明是按照Person的类型来约束类型的,但是调用的时候,访问的属性和方法是按照Guang的类型,自然类型是不安全的,所以会报错。但在ts2.x之前,是支持这种赋值的,即父类型可以赋值给子类型,子类型可以赋值给父类型,既逆变又协变,称为“双-方式协方差”。但是这样显然是有问题的,类型安全得不到保证,所以ts后面加了一个编译选项strictFunctionTypes,设置为true只支持函数参数的反转,设置为false则表示双向协变。关闭strictFunctionTypes后,我们会发现两种赋值都没问题:这支持函数参数的双向协变,类型检查不会报错,但不能严格保证类型安全。开启后,函数参数只支持倒置,子类型赋给父类型时会报错:这个倒置属性在类型编程中有什么用?还记得之前uniontointersection的实现吗?类型UnionToIntersection=(UextendsU?(x:U)=>unknown:never)extends(x:inferR)=>unknown?R:never类型参数U是要转换的联合类型。UextendsU是为了触发联合类型的分布式特性,让每个类型分别传入计算,最后合并。以U为参数构造一个函数,通过模式匹配得到参数的类型。结果是交集类型:我们通过构造多个函数类型,然后通过模式提取参数类型,实现了并集到交集。这里因为函数参数是逆变的,所以会返回联合类型的几种类型的子类型,这是一种更具体的交叉类型。反转和协变都是类型变化,都是针对父子类型的。非父子类型自然不会发生变化,即不变:非父子类型之间不会发生变化,只要类型不同就会报错:Howistheparent-childrelationshipbetweenthe类型确定?好像没看到extends的继承?类型父子关系的判断就像java中的类型是通过extends继承的。如果A扩展了B,那么A就是B的子类型。这被称为标称类型系统(nominaltype)。而ts是不看这个的,只要结构一致,那么就可以确定父子关系。这称为结构类型系统(structuraltype)。还是拿上面的例子来说:Guang和Person之间有extends关系吗?不是。那么亲子关系是怎么确定的呢?从结构上看,更具体的是亚型。这里的Guang具有Person的所有属性以及更多的属性,因此Guang是Person的子类型。注意这里用的是morespecific,不是more。在判断联合类型的父子关系时,更具体地说,'a'|'b'或'a'|'b'|'C'?'一个'|'b'更具体,所以'a'|'b'是'a'的子类型|'b'|'C'。测试中:总结ts通过给js加入静态类型系统来保证类型安全。大多数情况下,不同的类型不能赋值。但是,为了增加类型系统的灵活性,设计了父子类型的概念。父子类型之间自然要赋值,即会发生类型变化。类型变异分为逆变和协变。协变很好理解,就是子类型赋值给父类型。反转主要是函数赋值时函数参数的性质。参数的父类型可以分配给子类型。这是因为按照子类型声明的参数访问父类型的属性和方法没有问题,还是类型安全的。.但反过来就不一定了。但是在ts2.x之前,逆向仍然是可赋值的,即既有逆变也有协变,称为two-waycovariance。为了更严格的保证类型安全,ts增加了strictFunctionTypes编译选项。启用后,函数参数只支持逆变,否则支持双向协变。类型变化都是针对父子类型的,非父子类型自然不会变化或者保持不变。ts中父子类型的确定是根据结构来的,比较具体的是子类型。了解了如何判断父子类型(结构类型系统),以及父子类型变化(倒置、协变、双向协变)后,很多类型兼容问题就可以解释了。