TypeScript学习笔记——变型

111 阅读3分钟

定义

类型发生变化,变型都是发生在父子类型之间的。

结构化

截屏2024-02-09 下午5.16.12.png Typescript是结构性的类型系统(Structural typing),就是对值所具有的结构进行类型检查的。因此,判断两个类型是否可以兼容,只要看两个类型的结构是否兼容就可以,不需要关心类型的名称是否相同。

这也是Typescript的一个特点,在其它一些静态语言(如c#、java)这样赋值是会报错的,因为严格意义上不是同一个类型。所以在这个特点下,具有结构兼容性的两个类型我们可以认为是父子类型关系。

上面举的例子是interface的,那如果是联合类型呢?

截屏2024-02-09 下午5.35.05.png 从interface类型来看,属性多的属于子类型,而在联合类型则是成员多的属于父类。其实无论是什么类型,父类型所描述的类型范围是更加广泛的(父类属性少限制就少),而子类型描述的类型是更加狭窄的,从父类到子类其实是一个narrowing过程。

协变(Covariance)

在TS,两个类型结构化相似的类型,把子类型赋值给父类型依然是安全的这种称为协变。(可参考类型兼容性的例子)

可以这样简单的理解,鸭子类型 —— “看起来像只鸭子,那就是只鸭子”

逆变(Contravariance)

大多数的类型兼容都是协变,但是涉及到函数时会不一样。

interface Animal {
  age: number;
}

interface Dog extends Animal {
  bark(): void;
}

let animal: Animal;
let dog: Dog;

let visitAnimal = (animal: Animal) => {
    animal.age;
}
let visitDog = (dog: Dog) => {
    dog.age;
}

Dog属于是Animal的子类型,animal=dog赋值是安全的,那visitAnimal=visitDog呢?

是不可行的!假如把visitAnimal赋值为visitDog,那调用visitAnimal传入的参数类型可能是没有bark的Animal类型,运行时会报错,类型是不安全的;

但是visitDog=visitAnimal是可行的,因为调用visitDog时接收到的参数永远是有age的;

父子关系逆转了,这种就属于逆变。

双向协变

在Typescript中,由于灵活性等权衡,函数参数默认是双向协变的。就是既可以visitDog=visitAnimal也可以visitAnimal=visitDog。这个可以在tsconfig中设置strictFunctionType或者strict让函数参数以逆变校验。

默认开启双向协变的原因:

截屏2024-02-09 下午5.53.27.png

不变

不变就很好理解了,就是不允许变型。两个不能兼容,类型结构不一样的类型不能变型。

对infer的影响

interface Foo {
  (name: { a: string }): string;
  (name: { b: number }): number;
}

type BAR<T> = T extends {
  (name: infer R): infer Y;
  (name: infer R): infer Y;
}
  ? [R, Y]
  : never;
 
type BBB = BAR<Foo>;

截屏2024-02-09 下午6.02.51.png

简单来说,infer处于逆变位置推断出来的是交叉类型,处于协变位置推断出来的是联合类型。

使用例子

/**
 * 联合类型 => 交叉类型
 */
type UnionToIntersection<U> = (U extends any ? (arg: U) => void : never) extends (arg: infer P) => void ? P : never;

总结

不管是协变还是逆变,归根到底都是在保证类型安全的前提下,提供一些灵活性。使用Typescript时,类型安全前提下,可以接受与定义不一样的类型,也不用刻意去记住什么是协变、逆变,记住一些特性,遇到问题时有思路可以用即可,毕竟typescript是一个工具。

参考:

www.typescriptlang.org/docs/handbo…
github.com/sl1673495/b…
www.jianshu.com/p/38bf7fedc…