目录
1、用于类型守卫的工具类型和函数
2、用于联合类型的工具类型
3、用于Object类型的工具类型
4、UnionToIntersection
5、使用递归的工具类型
在上一篇文章《看懂复杂的TypeScript泛型运算》中,我们介绍了如何去用”函数“的思维去看懂TypeScript中的泛型和工具类型。本文作为上一篇的补充,主要内容是介绍并理解开源项目utility-types中更多的工具类型的具体实现。
用于类型守卫的工具类型和函数
这部分比较简单,直接上代码:
type Primitive =
| string
| number
| bigint
| boolean
| symbol
| null
| undefined;
export const isPrimitive = (val: unknown): val is Primitive => {
if (val === null || val === undefined) {
return true;
}
switch (typeof val) {
case 'string':
case 'number':
case 'bigint':
case 'boolean':
case 'symbol': {
return true;
}
default:
return false;
}
};
type Falsy = false | '' | 0 | null | undefined;
export const isFalsy = (val: unknown): val is Falsy => !val;
type Nullish = null | undefined;
export const isNullish = (val: unknown): val is Nullish => val == null;
Primitive、Falsy、Nullish都代表几种不同的原始类型构成的联合类型,分别对应三种不同的类型守卫。
用于联合类型的工具类型
这部分除了TypeScript中内置的Extract、Exclude、NonNullable外,还包含下列工具类型。
SetIntersection<A, B>
type SetIntersection<A, B> = A extends B ? A : never;
很明显,SetIntersection<A, B>的功能和实现和Extract<A, B>是相同的。
SetDifference<A, B>
type SetDifference<A, B> = A extends B ? never : A;
SetDifference<A, B>的功能和实现和Exclude<A, B>相同。
SetComplement<A, A1>
type SetComplement<A, A1 extends A> = SetDifference<A, A1>;
SetComplement<A, A1>和SetDifference<A, B>的不同之处在于,要求类型A1必须是A的子类型。
SymmetricDifference<A, B>
type SymmetricDifference<A, B> = SetDifference<A | B, A & B>;
单词symmetric意思为”对称的“,看个例子:
type Ta = '0' | '1' | '2' | () => void;
type Tb = '1' | '2' | '3' | Function;
type TResult = SymmetricDifference<Ta, Tb>; // Function | '0' | '3'
突然想到可以用高一数学的知识”集合“来理解联合类型,SymmetricDifference<A, B>可以得到两个集合(联合类型)中彼此不”相同“的类型。然而,这句话中的”相同“应该用extends运算来理解。
NonUndefined<A>
type NonUndefined<A> = A extends undefined ? never : A;
这个工具函数的实现和NonNullable<A>极其类似。即从联合类型A中排除undefined;
用于Object类型的工具类型
FunctionKeys<T>、NonFunctionKeys<T>
FunctionKeys<T>的作用是将T中值的类型为函数的对应的key提取出来,得到这些key构成的联合类型。实现如下:
type FunctionKeys<T extends object> = {
[K in keyof T]-?: NonUndefined<T[K]> extends Function ? K : never;
}[keyof T];
type Tc = {
a: string;
b: () => string;
c?: number;
d?: (number) => void;
}
type FunctionKeysResult = FunctionKeys<Tc>; // 'b' | 'd'
这个例子中,用[K in keyof T]表示类型T中的每一个key,用T[K]代表每一个key对应的value的类型,T[K]经过非undefined判断后,再判断是否是函数类型,若是则保留对应的key值。对这个例子中的Tc而言,首先得到如下类型:
type Tc2 = {
a: never;
b: 'b';
c: never;
d: 'd';
}
接下来计算type Tc3 = Tc2[keyof Tc]即可得到结果'b' | 'd'。
工具类型NonFunctionKeys<T>的作用恰好相反,得到的结果是T中非函数类型的value对应的key值构成的联合类型。实现方面也是类似的,只需要交换extends的结果即可。
type NonFunctionKeys<T extends object> = {
[K in keyof T]-?: NonUndefined<T[K]> extends Function ? never : K;
}
MutableKeys<T>、WritableKeys<T>、ReadonlyKeys<T>
type WritableKeys<T extends object> = MutableKeys<T>;
这两个工具类型的用法和实现是完全相同的,作用是返回类型T中所有可配置项(mutalble)的key构成的联合类型。实现如下:
type MutableKeys<T extends object> = {
[P in keyof T]-?: IfEquals<
{ [Q in P]: T[P] },
{ -readonly [Q in P]: T[P] },
P
>;
}[keyof T];
type ExampleMutableKeys = {
readonly foo: string; bar: number
}
type ResultMutableKeys = MutableKeys<ExampleMutableKeys>; // 'bar'
这里面用到了另一个工具类型IfEquals<X, Y, A, B>,先来看一看:
type IfEquals<X, Y, A = X, B = never> =
(<T>() => T extends X ? 1 : 2) extends (<T>() => T extends Y ? 1 : 2) ? A : B;
这个工具类型接受四个类型作为“参数”,其中后两个参数为可选的,所以可以先抛开不看,先考虑下面的工具类型:
type Equals<X, Y> =
(<T>() => T extends X ? 1 : 2) extends
(<T>() => T extends Y ? 1 : 2) ? true : false;
Equals<X, Y>这个工具类型的作用是判断泛型X和Y是否是相同的类型,是或否分别返回对应的布尔字面量类型。为什么这样写有作用目前我也不理解,这篇issue中提到了一个所谓TypeScript中内置的“isTypeIdenticalTo”检查,可能真正能解释运行原理的只有懂TypeScript源码的大佬了。
这个工具类型的功能并不完美,对于Equals<A & B, B & A>这种涉及交叉类型的判断,会得到错误的结果。
关于如何判断两个类型是否相同的问题,这里介绍另一个实现EqualsNotExact<X, Y>,但是这个实现更加不完美。这里使用了两个类型互相extends的方法,虽然在多数情况下是可行的。但是当其中的某一个类型是any时,结果一定为true,除非此时另一个类型是never。
type EqualsNotExact<X, Y> =
[X] extends [Y] ? (
[Y] extends [X] : true ? false
) ? false;
如果接受了Equals<X, Y>这个设定,IfEquals<X, Y, A, B>就不难理解了,相比之下只是更换了返回值,若X和Y是相同的类型则返回泛型A,否则返回泛型B。
接下来看MutableKeys<T>的实现。同样的,[P in keyof T]代表object类型T中的某一个key,接下来构造了新的object类型{ [Q in P]}: T[P] }作为X。这里面的P是一个字符串字面量类型,因此这个object类型的属性只有一项,就是P: T[P]这个键值对本身,T[P]依然代表这个key对应的value的类型。接下来构造新的object类型{ -readonly [Q in P]}: T[P] }作为Y,这个类型和前者的区别在于这个object中的唯一属性是可以配置的。接下来计算IfEquals<X,Y,P, never>,若X和Y是相同的类型,则X中的唯一属性为可配置项,因此返回对应的key。到了这一步,对于上面的例子ResultMutalbeKeys来说,我们得到了中间类型{ foo: never; bar: 'bar'},接下来进行如下计算,得到最终结果'bar'。
{ foo: never; bar: 'bar'}(keyof ExampleMutableKeys)
ReadonlyKeys<T>的效果和MutalbeKeys<T>相反,可以得到一个object类型的所有只读的属性的key构成的联合类型,实现如下。可以看出主要区别在于调换了IfEquals<X, Y, A, B>的后两个“参数”的位置。
type ReadonlyKeys<T extends object> = {
[P in keyof T]-?: IfEquals<
{ [Q in P]: T[P] },
{ -readonly [Q in P]: T[P] },
never,
P
>;
}[keyof T];
RequiredKeys<T>和OptionalKeys<T>
这两个工具函数同样功能恰好相反,因此我们可以先尝试理解OptionalKeys<T>。
type OptionalKeys<T extends object> = {
[K in keyof T]-?: {} extends Pick<T, K> ? K : never;
}[keyof T]
type ExampleOptionalKeys = {
foo: string;
prop: number;
optionalProp?: number;
optionalProp2?: () => void;
}
type ResultOptionalKeys = OptionalKeys<ExampleOptionalKeys>; // ‘optionalProp’ | 'optionalProp2'
其中的关键部分,在于对于object类型的每一个属性,都用Pick<T, K>的方式构造了一个新的object类型,如果一个空的object类型{}能够赋值,那么说明这个属性是可选的,则extends返回这个属性名本身。
相比之下,RequiredKeys<T>的区别,在于调换了extends表达式的两个返回值:
type RequiredKeys<T extends object> = {
[K in keyof T]-?: {} extends Pick<T, K> ? never : K;
}[keyof T]
PickByValue<T, ValueType>、PickByValueExact<T, ValueType>、OmitByValue<T, ValueType>、OmitByValueExact<T, ValueType>
PickByValue<T, ValueType>接受两个“参数”,第一个T是一个object类型,第二是任意的类型,最终得到T中属性类型和ValueType相同的属性构成的object类型。对于T中的每个属性的类型T[K],都使用extends来和ValueType做比较,若能够赋值,则认为这两个类型相同。进而得到符合要求的属性名构成的联合类型,结合Pick<T, U>,取得对应的属性,构成新的object类型。
type PickByValue<T, ValueType> = Pick<
T,
{ [K in keyof T]-?: T[K] extends ValueType ? K : never }[keyof T]
>;
type ExamplePickByValue = {
req: number;
reqUndef: number | undefined;
opt?: string;
}
type ResultPickByValue1 = PickByValue<ExamplePickByValue, number>;
// { req: number }
type ResultPickByValue2 = PickByValue<ExamplePickByBalue, number | undefined>;
// { req: number; reqUndfined: number | undefined }
然而,PickByValue<T, V>是有问题的,其仅仅使用了一次extends还不足以认为T[K]和ValueType是相同的类型,因此有了下面更加精确的工具类型PickByValueExact<T, ValueType>。这个工具类型中使用了两次extends,将T[K]和ValueType相互赋值给对方,只有两次都能够赋值,才认为两个类型相同。
type PickByValueExact<T, ValueType> = Pick<
T,
{
[K in keyof T]-?: [ValueType] extends [T[K]]
? [T[K]] extends [ValueType]
? K
: never
: never;
}[keyof T]
>;
type ExamplePickByValueExact = {
req: number;
reqUndef: number | undefined;
opt?: string;
}
type ResultPickByValueExact = PickByValueExact<ExamplePickByValue, number | undefined>;
// { reqUndfined: number | undefined }
想起来前面我们提到过名为EqualsNotExact<X, Y>的工具类型,也是使用两个类型相互extends来的方法来判断类型相同,因此可以利用Equals<X, Y>进行如下形式的改写。
type PickByValueExact2<T, V> = Pick<
T,
{ [K in keyof T]-?: EqualsNotExact<T[K], V> ? K : never }[keyof T]
>
在上一篇文章中,我们介绍了Pick<T, V>和Omit<T, V>在功能上是相反的。这里,同样有功能相反的工具类型,OmitByValue<T, ValueType>和OmitByValueExact<T, ValueType>,其实现如下。
type OmitByValue<T, ValueType> = Pick<
T, {
[K in keyof T]-?: T[K] extends ValueType ? never : K
}[keyof T]
>;
type OmitByValueExact<T, ValueType> = Pick<
T,
{
[K in keyof T]-?: [ValueType] extends [T[K]]
? [T[K]] extends [ValueType]
? never
: K
: K;
}[keyof T]
>;
Intersection<T, U>、Diff<T, U>
工具类型Intersection<T, U>的作用是从T中筛选出在U中同时存在的属性,通过这些共有的属性得到新的object类型。
type Intersection<T extends object, U extends object> = Pick<
T,
Extract<keyof T, keyof U> & Extract<keyof U, keyof T>
>;
type ExampleIntersection1 = { name: string; age: number: visible: boolean };
type ExampleIntersection2 = { age: number };
type ResultIntersection = Intersection<ExampleIntersection1, ExampleIntersection2>;
// { age: number }
这里首先限定T和U都必须是object类型,剩下的主体结构使用了”函数“Pick<T, U>。这个“函数”的第二个参数,是对keyof T和keyof U使用两次相互的Extract<T, U>运算,再取结果的联合类型,确保得到的结果是T和U中共有的属性名。最后共有的属性名,从T中pick出需要的结果。
工具类型Diff<T, U>的作用和Intersection<T, U>恰好相反,是从类型T中筛选出不在类型U中存在的属性。
type Diff<T extends object, U extends object> = Pick<
T,
SetDifference<keyof T, keyof U>
>;
type ExampleDiff1 = { name: string; age: number; visible: boolean; };
type ExampleDiff2 = { age: number };
type ResultDiff = Diff<ExampleDiff1, ExampleDiff2>;
// { name: string; visible: boolean; }
Subtract<T, T1>、OverWrite<T, U, I>、Assign<T, U, I>
工具函数Subtract<T, T1>要求T1必须是T的子类型,最终得到T中不存在于T1中的属性。
type Subtract<T extends T1, T1 extends object> = Pick<
T,
SetComplement<keyof T, keyof T1>
>;
工具类型OverWrite<T, U>的作用,是使用U中同名的属性覆盖类型T,最后得到一个object类型。这个工具类型的实现中,比较特别的一点在于,这个”函数“实际可以接受第三个”参数“I。此时有两种情况:
- 当没有第三个”参数“时,
I被默认赋值为Diff<T, U> && Intersection<U, T>,此时I已经是所需的结果,注意到Pick<I,keyof I>实际就是I本身 - 当有第三个”参数“时,前两个参数将不再发挥作用,直接用
I作为最终的结果
type OverWrite<
T extends object,
U extends object,
I = Diff<T, U> & Intersection<U, T>
> = Pick<I, keyof I>;
type ExampleOverWrite1 = { name: string; age: number; visible: boolean; };
type ExampleOverWrite2 = { age: string; other: string; };
type ResultOverWrite = OverWrite<ExampleOverWrite1, ExampleOverWrite2>;
// { name: string; age: string; visible: string }
工具类型Assign<T, U, I>的作用,是使用U中的所有的属性覆盖类型T,最后得到一个object类型。和OverWrite<T, U, I>的不同之处在于,还会覆盖T中不存在的属性,类似于函数Object.assign的功能。这个工具类型,也可以接受第三个”参数“,功能上和OverWrite<T, U, I>相同,这里不再赘述。
type Assign<
T extends object,
U extends object,
I = Diff<T, U> & Intersection<U, T> & Diff<U, T>
> = Pick<I, keyof I>;
type ExampleAssign1 = { name: string; age: number; visible: boolean; };
type ExampleAssign2 = { age: string; other: string; };
type ResultAssign = OverWrite<ExampleAssign1, ExampleAssign2>;
// { name: string; age: string; visible: boolean; other: string; }
Unionize<T>
工具类型Unionize<T>的作用,是讲一个object类型的每一个属性都提取出来构成一个新的object类型,最终得到若干个object类型构成的联合类型。实现方式和上文中的OptionalKeys<T>等十分类似。
type Unionize<T extends object> = {
[P in keyof T]: { [Q in P]: T[P] };
}[keyof T];
type ExampleUnionize = { name: string; age: number; visible: boolean; };
type ResultUnionize = Unionize<ExampleUnionize>;
// { name: string } | { age: number } | { visible: boolean }
PromiseType<T>
这个工具函数和上一篇文章中介绍的UnPromisify<T>完全相同,应用infer关键字,获取Promise的返回类型。
type PromiseType<T extends Promise<any>> = T extends Promise<infer U>
? U
: never;
Optional<T, K>、AugmentedRequired<T, K>、UnionToIntersection<U>
工具类型Optional<T, K>看上去是一种更高级的Partial<T>。对于类型T,接受另一个类型K表示需要设置为可选项的属性名。当没有第二个”参数“K时,这个工具类型的作用和Partial<T>就完全相同了。当有第二个”参数“K时,首先使用Oimt<T, K>把不在K中的属性筛选出来,再对剩余的属性做一次Partial操作,最后取联合类型。
type Optional<T extends object, K extends keyof T = keyof T> = Omit<
T,
K
> & Partial<Pick<T, K>>;
type ExampleOptional = { name: string; age: number; visible: boolean; }
type ResultOptional = Optional<ExampleOptional, 'age' | 'visible'>;
// { name: string; age?: number; visible?: boolean; }
工具类型AugmentedRequired<T, K>的作用和Optional<T, K>相反,可以理解为一种高级的Required<T>。对于类型T,将其中部分的属性值设置为必须的。在实现上,仅仅是把Partial<T>改为了Required<T, K>。
type AugmentedRequired<
T extends object,
K extends keyof T = keyof T
> = Omit<T, K> & Required<Pick<T, K>>;
type ExampleAugmentedRequired = { name?: string; age?: number; visible?: boolean; };
type ResultAugmentedRequired = AugmentedRequired<ExampleAugmentedRequired, 'age' | 'visible'>;
// { name?: string; age: number; visible: boolean; }
UnionToIntersection<U>
众所周知,typescript中有着联合类型和交叉类型,这个工具类型的作用,就是将联合类型转化成交叉类型。
type UnionToIntersection<U> = ( U extends any
? (k: U) => void
: never) extends (k: infer I) => void
? I
: never;
下面来考虑这个例子的实现方式,其中构造了两个函数类型(k: U) => void和(k: infer I) => void,其中第二个函数类型里面使用了infer关键字,来做类型推断。其中还使用了两个extends,第一个extends的右操作数是any,毫无疑问结果是true。第二个extends的结果也是true,但是这并不是重点。关键在于为什么(k: U) => void中的类型U作为一个联合类型,在(k: infer I) => void中推断出的新的类型I是一个交叉类型。
为了更好的理解,下面先介绍几个概念。
naked type
没有见过正式的中文翻译,这里姑且翻译为”裸露类型“好了。形如下面NakedType<T>这种形式的条件类型,就被成为naked type。
type NakedType<K> = K extends ...; // naked
type NotNakedType<K> = Wrap<K> extends ...; // not naked
naked type既然作为一种专门的类型,自然有其独有的性质。即,对于NakedType<T>而言,如果类型T是联合类型,比如A | B | C,则NakedType<A | B | C>和NakedType<A> | NakedType<B> | NakedType<C>是相同的类型。一句话总结,当泛型是几个类型构成的条件类型时,这个泛型的条件类型等于组成这个联合类型的条件类型的联合类型。
协变和逆变
这部分参考了What are covariance and contravariance?这篇文章。
covariance被翻译成”协变“,contravaricance被翻译成”逆变“。这两个概念不局限于TypeScript语言。对于大多数强类型语言而言,子类型的判断一直是一个值得讨论的话题。首先定义三个类型,动物、🐶和哈士奇,这三个类型有着明确的子类型关系。再定义一个函数类型TA,该类型的参数为Dog类型,返回值类型也为Dog类型。
interface Animal {
eat: () => void;
}
interface Dog extends Animal {
bark: () => void;
}
interface Husky extends Dog {
destroyHouse: () => void;
}
type TA = (k: Dog) => Dog
接下来需要考虑的是,如下的四个函数类型,哪一个类型是TA的子类型。
type T1 = (k: Husky) => Husky
type T2 = (k: Husky) => Animal
type T3 = (k: Animal) => Animal
type T4 = (k: Animal) => Husky
为了回答这个问题,需要再定义一个类型TF<F>,这个泛型类型用来判断某一个函数类型是否能够extends类型TA。
type TF<F> = F extends (k: Dog) => Dog ? true : false;
如果我们在tsconfig.json中配置了严格模式,则结果如下:
type TF1 = TF<T1> // false
type TF2 = TF<T2> // false
type TF3 = TF<T3> // false
type TF4 = TF<T4> // true
当对函数类型进行子类型判断时,要求函数的返回值必须是协变的,函数的参数必须是逆变的。所谓返回值类型是协变的,即若类型A是B的子类型,则函数类型(k: T) => A是函数类型(k: T) => B的子类型。所谓函数参数类型是逆变的,即若类型A是B的子类型,则函数类型(k: B) => T是函数类型(k: A) => T的子类型。因此,上面的四个例子中只有T4是TA的子类型。
此外,若没有给TypeScript配置严格模式或者没有单独配置--strictFunctionTypes=true,则函数的参数即可以是既是协变的,也是逆变的,官方Wiki称之为双变的(bivariant),此时T1也是TA的子类型,TF1的结果为true。
UnionToIntersection<T>
现在我们回到这个工具类型的讨论中来。定义几个类型,这几个类型用来表示不同清晰度下视频的地址,以及他们组成的联合类型。
type TUrl720p = { url720p: string };
type TUrl1080p = { url1080p: string };
type TUrl2k = { url2k: string };
type TUnionUrl = TUrl720p | TUrl1080p | TUrl2k;
带入到UnionToIntersection<T>中去,有:
type TIntersectionUrl = UnionToIntersection<TUnionUrl>;
// => 接下来有:
type TIntersectionUrl = UnionToIntersection<TUrl720p | TUrl1080p | TUrl2k>;
// => 由naked type的性质:
type TIntersectionUrl = UnionToIntersection<TUrl720p> | UnionToIntersection<TUrl1080p> | UnionToIntersection<TUrl2k>;
// => 展开:
type TIntersectionUrl =
(( TUrl720p extends any ? (k: U) => void
: never) extends (k: infer I) => void ? I : never) |
(( TUrl1080p extends any ? (k: U) => void
: never) extends (k: infer I) => void ? I : never) |
(( TUrl2k extends any ? (k: U) => void
: never) extends (k: infer I) => void ? I : never);
// => 进行第一个条件运算:
type TIntersectionUrl =
((k: TUrl720p) => void extends (k: infer I) => void ? I : never) |
((k: TUrl1080p) => void extends (k: infer I) => void ? I : never) |
((k: TUrl2k) => void extends (k: infer I) => void ? I | never); // 表达式A
// 执行infer
type IIntersectionUrl = TUrl720p | TUrl1080p | TUrl2k; // 表达式B
表达式B中得到的结果当然是错误的,三个infer出来的类型I是同一个I,当然不可能是不同的类型。注意到表达式A里面的TUrl720p、TUrl1080p和TUrl2k都处于逆变(contravariance)的位置。因此,类型I应当是这三个类型的子类型,因此只能是这三个类型组成的交叉类型。即type IIntersectionUrl = TUrl720p & TUrl1080p & TUrl2k。
使用递归的工具类型
递归的工具类型可以看做是上面非递归的工具类型的拓展,使用了多个extends运算,拓展了嵌套多层的object类型以及数组,每一个的实现方式都是大同小异,因此这里仅仅列出。
type DeepReadonly<T> = T extends ((...args: any[]) => any) | Primitive
? T
: T extends _DeepReadonlyArray<infer U>
? _DeepReadonlyArray<U>
: T extends _DeepReadonlyObject<infer V>
? _DeepReadonlyObject<infer V>
: T;
interface _DeepReadonlyArray<T> extends ReadonlyArray<DeepReadonly<T>> {};
type _DeepReadonlyObject<T> = {
readonly [P in keyof T]: DeepReadonly<T[P]>;
}
type DeepRequired<T> = T extends (...args: any[]) => any
? T
: T extends any[]
? _DeepRequiredArray<T[number]>
: T extends object
? _DeepRequiredObject<T>
: T;
interface _DeepRequiredArray<T> extends Array<DeepRequired<NonUndefined<T>>> {}
type _DeepRequiredObject<T> = {
[P in keyof T]-?: DeepRequired<NonUndefined<T[P]>>;
};
type DeepNonNullable<T> = T extends (...args: any[]) => any
? T
: T extends any[]
? _DeepNonNullableArray<T[number]>
: T extends object
? _DeepNonNullableObject<T>
: T;
interface _DeepNonNullableArray<T> extends Array<DeepNonNullable<NonNullable<T>>> {}
type _DeepNonNullableObject<T> = {
[P in keyof T]-?: DeepNonNullable<NonNullable<T[P]>>;
};
type DeepPartial<T> = T extends Function
? T
: T extends Array<infer U>
? _DeepPartialArray<U>
: T extends object
? _DeepPartialObject<T>
: T | undefined;
interface _DeepPartialArray<T> extends Array<DeepPartial<T>> {}
type _DeepPartialObject<T> = {
[P in keyof T]?: DeepPartial<T[P]>
};
type ValuesType<
T extends ReadonlyArray<any> | ArrayLike<any> | Record<any, any>
> = T extends ReadonlyArray<any>
? T[number]
: T extends ArrayLike<any>
? T[number]
: T extends object
? T[keyof T]
: never;
值得考虑一下的是,如果我们只需要一个针对object类型的递归工具类型,并不想考虑数组的情况。例如对于DeepPartial<T>,可以进行如下的改写NotSoDeepPartial<T>。
type NotSoDeepPartial<T> = T extends object
? {
[P in keyof T]?: NotSoDeepPartial<T[P]>
}
: T | undefined;
小结
本文按照个人的理解,介绍了开源项目utility-types中一系列工具类型的实现和用法。如果有什么意见或建议,欢迎留言讨论。
参考资料
1、看懂复杂的TypeScript泛型运算
2、TypeScript: Union to intersection type
3、What are covariance and contravariance?
4、TypeScript 2.8 Release Note