共计 5724 个字符,预计需要花费 15 分钟才能阅读完成。
最近在搬砖的时候,遇到一个场景,须要依据已存在的联结类型,将其转为穿插类型:
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
的接口,该接口有 name
、breed
属性。像上面这样:
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
是能够的,然而反过来不行。这是因为 Dog
是 Pet
的子类型。
继承就是实现多态性的一种形式。两个类型是继承关系,那么其子类型的变量,则与其父类型的变量存在可替代性关系,就像下面的 pet
与 dog
一样。
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
来示意。
协变
那所谓的协变是什么?
在类型编程中,咱们常常会将一个类型以泛型的模式传给另一个类型。
比如说咱们当初申明 PetList
与 DogList
:
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
。意味着 PetFunc
是 DogFunc
的子类型。
Dog
与 Pet
两个类型在通过 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>
既不为 true
,T<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…