你不知道的TypeScript工具类型

1,825 阅读16分钟

目录

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;

PrimitiveFalsyNullish都代表几种不同的原始类型构成的联合类型,分别对应三种不同的类型守卫

用于联合类型的工具类型

这部分除了TypeScript中内置的ExtractExcludeNonNullable外,还包含下列工具类型。

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>这个工具类型的作用是判断泛型XY是否是相同的类型,是或否分别返回对应的布尔字面量类型。为什么这样写有作用目前我也不理解,这篇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>就不难理解了,相比之下只是更换了返回值,若XY是相同的类型则返回泛型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>,若XY是相同的类型,则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 }

这里首先限定TU都必须是object类型,剩下的主体结构使用了”函数“Pick<T, U>。这个“函数”的第二个参数,是对keyof Tkeyof U使用两次相互的Extract<T, U>运算,再取结果的联合类型,确保得到的结果是TU中共有的属性名。最后共有的属性名,从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。此时有两种情况:

  1. 当没有第三个”参数“时,I被默认赋值为Diff<T, U> && Intersection<U, T>,此时I已经是所需的结果,注意到Pick<I,keyof I>实际就是I本身
  2. 当有第三个”参数“时,前两个参数将不再发挥作用,直接用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

当对函数类型进行子类型判断时,要求函数的返回值必须是协变的,函数的参数必须是逆变的。所谓返回值类型是协变的,即若类型AB的子类型,则函数类型(k: T) => A是函数类型(k: T) => B的子类型。所谓函数参数类型是逆变的,即若类型AB的子类型,则函数类型(k: B) => T是函数类型(k: A) => T的子类型。因此,上面的四个例子中只有T4TA的子类型。

此外,若没有给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里面的TUrl720pTUrl1080pTUrl2k都处于逆变(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