乐趣区

关于前端:Typescript-协变与逆变转换联合类型为交叉类型

最近在搬砖的时候,遇到一个场景,须要依据已存在的联结类型,将其转为穿插类型:

type Preson = {name: string} | {age: number} | {needMoney: boolean}

type Result = Uinon2Intersection<Preson>

冀望通过 Uinon2Intersection 转换后,失去的 Result

type Result = {name: string} & {age: number} & {needMoney: boolean}

刚开始感觉很简略。我想曾经会了类型体操根本动作四件套了。通过遍历联结类型,而后遍历的时候通过 key 读取属性值就行了,我啪啪啪就写进去了,就像这样:

type U2I<T> = {[key in keyof T]: T[key]
}

type Result = U2I<Preson>

理论失去的是:

type Result = U2I<{name: string;}> | U2I<{age: number;}> | U2I<{needMoney: boolean;}>

Nmmm,这齐全不是我冀望的样子啊,而后又想了想根底四件套,感觉遇到坑了,如同仅靠四件套并不能解决啊。

先说下,下面这种状况是因为 对于联结类型,在遍历操作或者进行条件类型判断的时候,会产生类型调配。就像上面:

type ToArray<Type> = Type extends any ? Type[] : never;

type StrArrOrNumArr = ToArray<string | number>;

其实失去:

type StrArrOrNumArr = string[] | number[]

如果想得到: (string | number)[]。你须要这么写:

type ToArrayNonDist<Type> = [Type] extends [any] ? Type[] : never;

回到注释,如果说咱们想通过一个工具类型实现联结类型到穿插类型的转换,那须要理解一下上面几个 ts 要害概念:协变、逆变、双向协变、不变性。

类型兼容与可替代性

咱们先说说类型兼容与可替代性,因为这两个概念与协变、逆变密切相关。

Typescript 的类型兼容个性是基于类型构造的,其根本规定是:如果 y 类型至多有一些与 x 类型雷同的成员,则 x 类型与 y 类型兼容

例如,构想一个名为 Pet 的接口的代码,该接口有一个 name 属性;一个名为 Dog 的接口,该接口有 namebreed 属性。像上面这样:

interface Pet {name: string;}
interface Dog {
  name: string;
  breed: string
}

let pet: Pet;
// dog's inferred type is {name: string; owner: string;}
let dog: Dog = {name: "大哥", breed: "罗威纳"};
pet = dog;

dog 对象中存在 Pet 接口中最根本的属性成员 name。则 dog 能够赋值给 pet 变量。

这是因为 Dog 属性中存在与 Pet 雷同的属性。

咱们写一个 IsSubTyping 工具类型,用于判断类型之间的继承关系:

type IsSubTyping<T, U> = T extends U ? true : false

type R0 = IsSubTyping<Dog, Pet> // true

通过下面代码,咱们失去的 R = true。这阐明,尽管咱们没有显示的申明 Dog extends Pet,然而 ts 外部判断这两个接口是兼容且继承的。

其实更符合规范的就是显示申明 继承

interface Pet {name: string;}

interface Dog extends Pet {breed: string;}

let pet: Pet = {name: "宠物"};
let dog: Dog = {name: "大哥", breed: "罗威纳"};

pet = dog; // Ok
dog = pet; // Error
// Property 'breed' is missing in type 'Animal' but required in type 'Dog'.

dog 赋值给 animal 是能够的,然而反过来不行。这是因为 DogPet 的子类型。

继承就是实现多态性的一种形式。两个类型是继承关系,那么其子类型的变量,则与其父类型的变量存在可替代性关系,就像下面的 petdog 一样。

pet 变量能够承受 dog 变量。

这个个性在 函数传参 时十分便当:

function logName(pet: Pet) {console.log(pet.name)
}

logName(pet) // Ok
logName(dog) // Ok

logName 函数的参数为 Pet 类型,则其也能够承受 Pet 的子类型。这就是类型的可替代性,在理论开发中,你肯定曾经有意识的应用到这一个性了。

type T1 = IsSubTyping<'hello', string>; // true
type T2 = IsSubTyping<42, number>; // true
type T3 = IsSubTyping<Map<string, string>, Object>; // true

这里咱们引入一个符号 <:。如果 A 是 B 的子类型,则咱们能够应用 A <: B 来示意。

协变

那所谓的协变是什么?

在类型编程中,咱们常常会将一个类型以泛型的模式传给另一个类型。

比如说咱们当初申明 PetListDogList

type PetList = Array<Pet>
type DogList = Array<Dog>

type T4 = IsSubTyping<DogList, PetList> // true

这里产生一个十分有意思的景象:

Dog <: Pet,则 DogList <: PetList 也是成立的。

为此咱们这样定义这一个性:

如果某个类型 T 能够保留其余类型之间的关系,那么它就是可协变的。即如果 A <: B,则 T<A> <: T<B>

在 ts 中常见的一些可协变类型:

  • Promise
type T5 = IsSubTyping<Promise<Dog>, Promise<Pet>> // true
  • Record
type T6 = IsSubTyping<Record<string, Dog>, Record<string, Pet>> // true
  • Map
type T7 = IsSubTyping<Map<string, Dog>, Map<string, Pet>> // true

逆变

逆变与协变相同,它能够反转两个类型之间的关系:

如果某种类型 T 能够反转其余类型之间的关系,那么它就是可逆变的。即如果 A <: B,则 T<A> :> T<B>成立。

这种状况通常产生在泛型函数中,咱们定义一个泛型函数类型:

type Func<Param> = (param: Param) => void

咱们晓得 Dog <: Pet。则当咱们将这两个接口传给 Func 类型后,获取的类型关系时,是怎么的?

让咱们试一试:

type PetFunc = Func<Pet>
type DogFunc = Func<Dog>

type T8 = IsSubTyping<DogFunc, PetFunc> // false

type T9 = IsSubTyping<PetFunc, DogFunc> // true

IsSubTyping<PetFunc, DogFunc> 返回 true。意味着 PetFuncDogFunc 的子类型。

DogPet 两个类型在通过 Func 解决后,继承关系产生了反转,咱们就说 Func<T> 是可逆变的。

通常函数类型在解决参数的时候都会产生逆变。函数类型的父子类型关系与参数类型的父子关系相同。

type FuncPet = (pet: Pet) => void
type FuncDog = (dog: Dog) => void

type T10 = IsSubTyping<Dog, Pet> // true
type T11 = IsSubTyping<PetFunc, DogFunc> // true

其实到这里咱们曾经能够利用逆变的个性,解决结尾提到的 将一个联结类型转为穿插类型 需要了:

  • 一个联结类型肯定与其对应的穿插类型兼容:
type S = {name: string} | {age: number} | {needMoney: boolean}

type I = {name: string} & {age: number} & {needMoney: boolean}

type IsSub = IsSubTyping<I, S> // true
  • 为此能够利用泛型函数类型参数会产生逆变的个性,实现工具类型 U2I
type U2I<U> =
  (U extends unknown ? (k: U) => void : never) extends ((k: infer I) => void) ? I : never

在下面的示例中,咱们将泛型利用于了函数的参数,由此产生了逆变。如果咱们将泛型利用于函数返回值呢? 是会产生逆变还是协变?

type PetFunc<Pet> = () => Pet
type DogFunc<Dog> = () => Dog

type T12 = IsSubTyping<Dog, Pet> // true
type T13 = IsSubTyping<PetFunc<Pet>, DogFunc<Dog>> // false
type T14 = IsSubTyping<PetFunc<Dog>, DogFunc<Pet>> // true

通过下面的代码,咱们能够晓得下面泛型类型的函数返回值产生了协变。由此能够晓得,函数类型的非凡之处在于其联合了逆变与协变:参数会产生逆变而返回值类型会返回协变。

双向协变

当类型 T 能够使其余类型之间即产生协变又产生逆变的关系,咱们称之为双向协变。双向协变与 strictFunctionTypes 配置项的开启相干,当 strictFunctionTypes 没有开启时,上面的代码不会给出任何谬误提醒。

开启双向协变,你须要在 Ts Config 中敞开 strictFunctionTypes

type PrintFn<T> = (arg: T) => void

interface Pet {name: string;}

interface Dog extends Pet {breed: string;}

declare let f1: PrintFn<Pet>; // f1: (x: Animal) => void
declare let f2: PrintFn<Dog>; // f2: (x: Dog) => void

f1 = f2; // Ok
f2 = f1; // Ok

如果在 ts config 中开启 strictFunctionTypes,当进行 f1 = f2 操作时,会给出如下谬误提醒:

Type 'PrintFn<Dog>' is not assignable to type 'PrintFn<Pet>'.
  Property 'breed' is missing in type 'Pet' but required in type 'Dog'.

如果依照继承的思维, Dog 类型显著属于 Pet 类型,所以 PrintFn<Dog> 齐全能够赋值给 PrintFn<Pet>

可是当你开启 strictFunctionTypes 配置的时候,函数类型参数的地位被 ts 限度为是逆变,而非双向协变。

ts 编译器会在赋值函数对象时,对函数的参数和返回值执行子类型兼容性查看,发现 Dog <: Pet,进行逆变操作应该为 PrintFn<Pet> <: PrintFn<Dog>,而不是 PrintFn<Dog> <: PrintFn<Pet>,故给出谬误提醒。

对于函数参数双向协变感兴趣的能够,跳转:

https://github.com/Microsoft/TypeScript/wiki/FAQ#why-are-function-parameters-bivariant

不变性

所谓不变性是指类型 T 既不会让两个类型之间产生协变,也不会产生逆变。如果 A <: B, 则 T<A> <: T<B> 既不为 trueT<B> <: T<A> 也不为 true.

以上面代码为例

type IdentityFn<T> = (arg: T) => T;

type T13 = SubtypeOf<IdentityFn<Pet>, IdentityFn<Dog>>; // false
type T14 = SubtypeOf<IdentityFn<Dog>, IdentityFn<Pet>>; // false

let petFn: IdentityFn<Pet> = (pet: Pet) => {return pet;};

let dogFn: IdentityFn<Dog> = (dog: Dog) => {return dog;};

petFn = dogFn; // Error
dogFn = petFn; // Error

上述代码报错,是因为一个是因为协变检测失败,一个是因为逆变检测失败,所以函数类型 IdentityFn 既不反对协变,也不反对逆变。

总结

通常来说,当你理解了 Ts 的类型兼容个性后,协变与逆变是十分好了解的。协变与逆变的呈现,都是为了类型拜访的安全性。协变类型,相似于类型的属性膨胀,仅需满足根本的类型构造,即可保障类型属性的拜访平安,实现继承关系;也而逆变类型,通常产生在泛型函数类型中,而函数会多一层拜访空间,ts 并不会晓得用户将来会拜访参数的哪些属性,则平安的做法就是进行类型属性扩大,也就是逆变。

参考:

  • https://www.typescriptlang.org/docs/handbook/type-compatibili…
  • https://www.typescriptlang.org/docs/handbook/2/conditional-ty…
  • https://dmitripavlutin.com/typescript-covariance-contravariance/
  • https://stackoverflow.com/questions/66410115/difference-between-variance-covariance-contravariance-and-bivariance-in-typesc
  • https://www.jihadwaspada.com/post/covariance-and-contravarian…
  • https://javascript.plainenglish.io/never-fear-ts-covariant-co…
退出移动版