首页 > 其他分享 >快速理解 TypeScript 的逆变和协变

快速理解 TypeScript 的逆变和协变

时间:2023-10-14 15:48:46浏览次数:35  
标签:TypeScript Guang 逆变 Person 协变 类型 赋值

快速理解 TypeScript 的逆变和协变

发布于 2022-06-06 08:36:42 8081 举报

深入学习 TypeScript 类型系统的话,逆变、协变、双向协变、不变是绕不过去的概念。

这些概念看起来挺高大上的,其实并不复杂,这篇文章我们就来学习下它们吧。

类型安全和型变

TypeScript 给 JavaScript 添加了一套静态类型系统,是为了保证类型安全的,也就是保证变量只能赋同类型的值,对象只能访问它有的属性、方法。

比如 number 类型的值不能赋值给 boolean 类型的变量,Date 类型的对象就不能调用 exec 方法。

这是类型检查做的事情,遇到类型安全问题会在编译时报错。

但是这种类型安全的限制也不能太死板,有的时候需要一些变通,比如子类型是可以赋值给父类型的变量的,可以完全当成父类型来使用,也就是“型变”(类型改变)。

这种“型变”分为两种,一种是子类型可以赋值给父类型,叫做协变,一种是父类型可以赋值给子类型,叫做逆变。

先来看下协变:

协变

其中协变是很好理解的,比如我们有两个 interface:

interface Person {
    name: string;
    age: number;
} 

interface Guang {
    name: string;
    age: number;
    hobbies: string[]
}

这里 Guang 是 Person 的子类型,更具体,那么 Guang 类型的变量就可以赋值给 Person 类型:

这并不会报错,虽然这俩类型不一样,但是依然是类型安全的。

这种子类型可以赋值给父类型的情况就叫做协变。

为什么要支持协变很容易理解:类型系统支持了父子类型,那如果子类型还不能赋值给父类型,还叫父子类型么?

所以型变是实现类型父子关系必须的,它在保证类型安全的基础上,增加了类型系统的灵活性。

逆变相对难理解一些:

逆变

我们有这样两个函数:

let printHobbies: (guang: Guang) => void;

printHobbies = (guang) => {
    console.log(guang.hobbies);
}

let printName: (person: Person) => void;

printName = (person) => {
    console.log(person.name);
}

printHobbies 的参数是 printName 参数的子类型。

那么问题来了,printName 能赋值给 printHobbies 么?printHobbies 能赋值给 printName 么?

测试一下发现是这样的:

printName 的参数Person不是 printHobbies 的参数Guang的父类型么,为啥能赋值给子类型?

因为这个函数调用的时候是按照 Guang 来约束的类型,但实际上函数只用到了父类型 Person 的属性和方法,当然不会有问题,依然是类型安全的。

这就是逆变,函数的参数有逆变的性质(而返回值是协变的,也就是子类型可以赋值给父类型)。

那反过来呢,如果 printHoobies 赋值给 printName 会发生什么?

因为函数声明的时候是按照 Person 来约束类型,但是调用的时候是按照 Guang 的类型来访问的属性和方法,那自然类型不安全了,所以就会报错。

但是在 ts2.x 之前支持这种赋值,也就是父类型可以赋值给子类型,子类型可以赋值给父类型,既逆变又协变,叫做“双向协变”。

但是这明显是有问题的,不能保证类型安全,所以之后 ts 加了一个编译选项 strictFunctionTypes,设置为 true 就只支持函数参数的逆变,设置为 false 则是双向协变。

我们把 strictFunctionTypes 关掉之后,就会发现两种赋值都可以了:

这样就支持函数参数的双向协变,类型检查不会报错,但不能严格保证类型安全。

开启之后,函数参数就只支持逆变,子类型赋值给父类型就会报错:

在类型编程中这种逆变性质有什么用呢?

还记得之前联合转交叉的实现么?

type UnionToIntersection<U> = 
    (U extends U ? (x: U) => unknown : never) extends (x: infer R) => unknown
        ? R
        : never

类型参数 U 是要转换的联合类型。

U extends U 是为了触发联合类型的 distributive 的性质,让每个类型单独传入做计算,最后合并。

利用 U 做为参数构造个函数,通过模式匹配取参数的类型。

结果就是交叉类型:

我们通过构造了多个函数类型,然后模式提取参数类型的方式,来实现了联合转交叉,这里就是因为函数参数是逆变的,会返回联合类型的几个类型的子类型,也就是更具体的交叉类型。

逆变和协变都是型变,是针对父子类型而言的,非父子类型自然就不会型变,也就是不变:

不变

非父子类型之间不会发生型变,只要类型不一样就会报错:

那类型之间的父子关系是怎么确定的呢,好像也没有看到 extends 的继承?

类型父子关系的判断

像 java 里面的类型都是通过 extends 继承的,如果 A extends B,那 A 就是 B 的子类型。这种叫做名义类型系统(nominal type)。

而 ts 里不看这个,只要结构上是一致的,那么就可以确定父子关系,这种叫做结构类型系统(structual type)。

还是拿上面那个例子来说:

Guang 和 Person 有 extends 的关系么?

没有呀。

那是怎么确定父子关系的?

通过结构,更具体的那个是子类型。这里的 Guang 有 Person 的所有属性,并且还多了一些属性,所以 Guang 是 Person 的子类型。

注意,这里用的是更具体,而不是更多。

判断联合类型父子关系的时候, 'a' | 'b' 和 'a' | 'b' | 'c' 哪个更具体?

'a' | 'b' 更具体,所以 'a' | 'b' 是 'a' | 'b' | 'c' 的子类型。

测试下:

总结

ts 通过给 js 添加了静态类型系统来保证了类型安全,大多数情况下不同类型之间是不能赋值的,但是为了增加类型系统灵活性,设计了父子类型的概念。父子类型之间自然应该能赋值,也就是会发生型变。

型变分为逆变和协变。协变很容易理解,就是子类型赋值给父类型。逆变主要是函数赋值的时候函数参数的性质,参数的父类型可以赋值给子类型,这是因为按照子类型来声明的参数,访问父类型的属性和方法自然没问题,依然是类型安全的。但反过来就不一定了。

不过 ts 2.x 之前反过来依然是可以赋值的,也就是既逆变又协变,叫做双向协变。

为了更严格的保证类型安全,ts 添加了 strictFunctionTypes 的编译选项,开启以后函数参数就只支持逆变,否则支持双向协变。

型变都是针对父子类型来说的,非父子类型自然就不会型变也就是不变。

ts 中父子类型的判定是按照结构来看的,更具体的那个是子类型。

理解了如何判断父子类型(结构类型系统),父子类型的型变(逆变、协变、双向协变),很多类型兼容问题就能得到解释了。

标签:TypeScript,Guang,逆变,Person,协变,类型,赋值
From: https://www.cnblogs.com/sexintercourse/p/17764230.html

相关文章

  • typescript: Visitor Pattern
     /****VisitorPattern访问者是一种行为设计模式,允许你在不修改已有代码的情况下向已有类层次结构中增加新的行为。*file:Visitorts.ts*TheComponentinterfacedeclaresan`accept`methodthatshouldtakethebase*visitorinterfaceasanargument.......
  • typescript: Strategy Pattern
     /***StrategyPattern策略是一种行为设计模式,它将一组行为转换为对象,并使其在原始上下文对象内部能够相互替换。**file:Strategyts.ts*TheContextdefinestheinterfaceofinteresttoclients.*/classGeovinContext{/***@type{GeovinSt......
  • TypeScript数据类型
    TypeScript数据类型:string:字符串number:数字boolean:true/falsestring[]:数组any:可以是任何类型。当你不希望某个特定的值导致类型检查错误时,你可以使用它。以下都不会报编译异常letvalue:any;value.foo.bar;//OKvalue.trim();//OKvalue();//OKnewvalue();//OKvalue[0][......
  • typescript: Observer Pattern
     /***ObserverPattern观察者是一种行为设计模式,允许一个对象将其状态的改变通知其他对象*file:Observerts.ts*TheSubjectinterfacedeclaresasetofmethodsformanagingsubscribers.*/interfaceGeovinSubject{//Attachanobservertothesub......
  • typescript: Mediator pattern
     /****Mediatorpattern中介者是一种行为设计模式,让程序组件通过特殊的中介者对象进行间接沟通,达到减少组件之间依赖关系的目的。*file:Mediatorts.ts*TheMediatorinterfacedeclaresamethodusedbycomponentstonotifythe*mediatoraboutvarious......
  • typescript: Template Method pattern
     /***TemplateMethodpattern模版方法是一种行为设计模式,它在基类中定义了一个算法的框架,允许子类在不修改结构的情况下重写算法的特定步骤。*file:Templatets.ts*TheAbstractClassdefinesatemplatemethodthatcontainsaskeletonofsome*algorithm,......
  • typescript: State Pattern
     /***StatePattern状态是一种行为设计模式,让你能在一个对象的内部状态变化时改变其行为。*TheContextdefinestheinterfaceofinteresttoclients.Italsomaintainsa*referencetoaninstanceofaStatesubclass,whichrepresentsthecurrent*stat......
  • typescript: Iterator Pattern
     /***IteratorPattern迭代器是一种行为设计模式,让你能在不暴露复杂数据结构内部细节的情况下遍历其中所有的元素*file:Iteratorts.tsnpminstall-gbabel-cli*Intent:Letsyoutraverseelementsofacollectionwithoutexposingits*underlyingrepres......
  • typescript: Memento Pattern
     /***MementoPattern备忘录是一种行为设计模式,允许生成对象状态的快照并在以后将其还原。*TheOriginatorholdssomeimportantstatethatmaychangeovertime.Italso*definesamethodforsavingthestateinsideamementoandanothermethodfor*re......
  • typescript: Chain of Responsibility Pattern
     /***ChainofResponsibilityPattern责任链是一种行为设计模式,允许你将请求沿着处理者链进行发送,直至其中一个处理者对其进行处理。*file:Chaints.ts*TheHandlerinterfacedeclaresamethodforbuildingthechainofhandlers.*Italsodeclaresameth......