源码解读utility-types

做者:王冲html

GitHub: github.com/hubvuevue

以前有系统的学过 TypeScript,而且能够在项目中使用一些基本的类型定义,可是对于高级类型却只知其一;不知其二,看到一些项目或者库中写的高级类型彻底是懵逼的状态,因而就决定想办法去改变这种状态。忘记是哪位大神说过:看源码是最好的学习方式,因而就决定找个专门作 TypeScript 类型的库读读源码。经过同事推荐了两个比较好的库:utility-typests-toolbelt,权衡下utility-typesstar 比较多而且用的也比较多,那就它吧,以后再对ts-toolbelt进行解读。git

本篇文章主要是对mapped-types.ts文件中的类型进行解读。github

SetIntersection

在 Typescript 内置的类型 API 中有个 Extract 类型,SetIntersection 类型的做用于 Extract 是相同,做用都是从类型 A 中获取可兼容类型 B 的类型,大体意思就是获取两个类型的交集。多用于联合类型。typescript

内置的 Extract 类型的实现方式和 SetIntersection 是相同的json

实现数组

type SetIntersection<A, B> = A extends B ? A : never
复制代码

示例markdown

type SetIntersectionResult = SetIntersection<'a' | 'b' | 'c', 'c' | 'b'> // 'b' | 'c'
复制代码

上面示例结果是怎么获得的呢?咱们都知道条件类型做用于联合类型上会变成分布式条件类型,结合上面示例和源码解释下:app

'a' | 'b' | 'c' extends 'c' | 'b' ? 'a' | 'b' | 'c' : never =>
('a' extends 'c' | 'b' ? 'a' : never) |
('b' extends 'c' | 'b' ? 'b' : never) |
('c' extends 'c' | 'b' ? 'c' : never) =>
never | 'b' | 'c' => 'b' | 'c'
复制代码

SetDifference

与 TypeScript 内置的 Exclude 类型相同,SetDifference 类型用于获取类型 A 中不可兼容类型 B 的类型 ,大体意思是取类型 B 在类型 A 上的补集,多用于联合类型。分布式

实现

type SetDifference<A, B> = A extends B ? never : A
复制代码

示例

type SetDifferenceResult = SetDifference<'a' | 'b' | 'c', 'b'> // 'a' | 'c'
复制代码

上面示例结果是怎么获得的呢?其实和上一个类型的运算结果大体相同,结合示例和源码解释下:

'a' | 'b' | 'c' extends 'b' ? never : 'a' | 'b' | 'c' =>
('a' extends 'b' ? never : 'a') |
('b' extends 'b' ? never : 'b') |
('c' extends 'b' ? never : 'c') =>
'a' | never | 'c' => 'a' | 'c'
复制代码

源码里还有个类型SetComplement,可是实现方式和SetDifference相同,只是约束了泛型 B 必须为泛型 A 的子类型,具体就不分析了。

type SetComplement<A, A1 extends A> = A extends A1 ? never : A
复制代码

SymmetricDifference

SymmetricDifference用于获取类型 A、B 的交集在并集上的补集,多用于联合类型。

实现

type SymmetricDifference<A, B> = SetDifference<A | B, SetIntersection<A, B>>
复制代码

emmmm...有点绕,看个 🌰 吧

type SymmtricDifferenceResult = SymmetricDifference<
  '1' | '2' | '3',
  '2' | '3' | '4'
> // '1' | '4'
复制代码

例子中两个类型并集为: '1' | '2' | '3' | '4',交集为'2' | '3',所以交集在并集上的补集为'1' | '4'

是怎么作到的呢?从源码中能够看出来,咱们用到了SetDifferenceSetIntersection两个类型,而且这两个类型是在以前实现过的,经过组合的方式造成一个功能更增强大的类型。

源码中的解法是这样的:经过 A|B获取到 A、B 类型的并集,而后再经过SetIntersection类型获取到 A、B 类型的交集,最后再使用SetDifference类型求补集得出结果。

NonUndefined

NonUndefined类型用于过滤掉联合类型中的 undefined 类型。

实现

type NonUndefined<T> = T extends undefined ? never : T
复制代码

源码中的实现是上面这样的,下面是借用SetDifference的实现。

type NonUndefined<T> = SetDifference<T, undefined>
复制代码

示例

type NonUndefinedResult = NonUndefined<string | null | undefined> // string | null
复制代码

想要看到上面效果,你须要在tsconfig.json中将strictNullChecks设置为true,严格来检查null类型,若是不开启的话ts就默认undefined与null是兼容的,因此就会将null类型过滤掉。

FunctionKeys

FunctionKeys用于获取对象类型中值为函数的 key。

实现

type FunctionKeys<T extends object> = {
  [K in keyof T]-?: NonUndefined<T[K]> extends Function ? K : never
}[keyof T]
复制代码

源码里是上面这样实现的,可是有些缺陷,在分析原理的时候再说为何时候缺陷的。

示例

type MixedProps = {
  name: string
  setName: (name: string) => void
  someKeys?: string
  someFn?: (...args: any) => any
  undef: undefined
  unNull: null
}
type FunctionKeysResult = FunctionKeys<MixedProps> //"setName" | "someFn" | "undef" 
复制代码

咦,不该该是"setName" | "someFn"么,为何多了两个呢?咱们先来分析一下这个类型是怎么实现的,在分析过程当中找 bug。

FunctionKeys接受的是一个对象类型,所以可使用索引查询操做符遍历对象类型的每个 key 值,遍历过程当中首先经过NonUndefined过滤掉 undefined 类型,而后 extends Function,检测可兼容 Function 类型,那么这个 key 的值类型就是一个函数类型,可是当值类型为 undefined 的时候,会被NonUndefined解为 never,然而 Function 类型是兼容 never 的。因此undef就被保留了下来。

因而我在源码的基础上改了改。

type FunctionKeys<T extends object> = {
  [P in keyof T]-?: SetIntersection<NonNullable<T[P]>, Function> extends never
    ? never
    : P
}[keyof T]
复制代码

具体思路是在遍历过程当中先将值类型为 undefined、null 的 key 的值类型转为 never,而后再与 Function 取交集,也就是说将全部值类型不是函数类型的都转为 never,因为 never 类型只对自身兼容,因此再判断值类型是否兼容 never 类型,将全部的值为 never 类型的 key 过滤掉,最后再经过索引查询操做符获取到值类型的联合类型便可。

NonFunctionKeys

NonFunctionKeys用于获取对象类型中值不为函数的 key

实现

type NonFunctionKeys<T extends Object> = {
  [P in keyof T]-?: NonUndefined<T[P]> extends Function ? never : P
}[keyof T]
复制代码

示例

type NonFunctionKeysResult = NonFunctionKeys<MixedProps> //"name" | "someKeys" | "unNull"
复制代码

通过FunctionKeys类型的分析,NonFunctionKeys类型应该就很好理解了。

在遍历对象类型的过程当中,先使用NonUndefined过滤掉值类型为 undefined 的 key,而后再过滤掉值类型为函数类型的 key,最后经过索引查询操做符获取到值类型的联合类型便可。

IfEquals

IfEquals 是一个辅助类型函数,用于判断两个类型是否相同。

实现

type IfEquals<X, Y, A = X, B = never> = (<T>() => T extends X ? 1 : 2) extends <
  T
>() => T extends Y ? 1 : 2
  ? A
  : B
复制代码

若是你了解一些 TS 的话可能会想到,判断两个类型是否相同不是直接使用双向 extends 就能够了吗,这个是什么玩意?🤔️

我想你说的双向 extends 方式是这样的。

type Same<X, Y> = X extends Y ? (Y extends X ? true : false) : false
复制代码

对于上面 Same 类型函数这种写法,实际上是有缺陷的,它没有办法推断两个类型是否绝对相同,好比说相同结构但带有不一样属性修饰符的对象类型。

type X = {
  name: string
  age: number
}
type Y = {
  readonly name: string
  age: number
}
复制代码

上面这两个类型 Same 类型函数就没法推断,这种状况下就必需要使用IfEquals类型函数了。

示例

type SameResult = Same<X, Y> //true
type IfEqualsResult = IfEquals<X, Y> //never
复制代码

IfEquals类型函数的核心就是使用了延时条件类型,在兼容性推断的时候依赖了内部类型的一致性检查。IfEquals内部最少依赖了两个泛型参数,XY,在传入XY泛型参数后,对类型进行推断,若是能推断出结果就返回最终的类型,不然就延时推断过程,等待确认的类型参数传进来后再进行类型推断。

IfEquals类型函数同样,构造一个延时条件类型很简单,只须要构建一个函数类型而且将函数的返回值构建成依赖泛型参数的条件类型就能够了。

type DeferConditionalType = <T>(value: T) => T extends string ? number : boolean
复制代码

在使用DeferConditionalType泛型的时候就会根据传入的泛型参数延时推断出返回值类型。

WriteableKeys

WriteableKeys 用于获取对象类型中全部可写的 key。

实现

export type WriteableKeys<T extends object> = {
  [P in keyof T]-?: IfEquals<
    { [Q in P]: T[P] },
    { -readonly [Q in P]: T[P] },
    P
  >
}[keyof T]
复制代码

示例

type Props = { readonly foo: string; bar: number }

type WriteableKeysResult = WriteableKeys<Props> // "bar"
复制代码

从源码中能够看出使用了 IfEquals 函数,如今咱们已经知道 IfEquals 函数用于判断两个类型是否严格相等(不清楚的能够看下 IfEquals 函数的解析),因此就比较好办了。

在遍历对象 key 的过程当中,构造两个对象,分别是原 key 构造的对象和去掉 readonly 修饰 key 构造的对象,而且第三个参数传入 key,做为匹配相同的类型函数返回值,所以最终结果就是带有 readonly 修饰的 key 的值类型都是 never,其他的 key 的值类型是 key 自己,最后再经过索引类型访问操做符获取到全部 key 的值类型的联合类型。

ReadonlyKeys

ReadonlyKeys 用于获取对象类型中全部被 readonly 修饰的 key。

实现

export 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]
复制代码

示例

type Props = { readonly foo: string; bar: number }

type ReadonlyKeysResult = ReadonlyKeys<Props> // "foo"
复制代码

ReadonlyKeys 的实现方式和 WriteableKeys 的实现方式基本相同,区别在于 IfEquals 函数的第3、四个参数。在 WriteableKeys 中,第三个参数是 key,第四个参数默认是 never,而在 ReadonlyKeys 中颠倒过来了,缘由是,当两个类型匹配成功后,则认定这两个类型是严格相同的,那么就表示当前 key 是不被 readonly 修饰的,因此在 WriteableKeys 中返回 key、在 ReadonlyKeys 中返回 never;当两个类型匹配不成功后,则认定这两个类型是不相同的。

RequiredKeys RequiredKeys 用于获取对象类型中全部必选的 key。

实现

export type RequiredKeys<T extends object> = {
  [P in keyof T]-?: {} extends Pick<T, P> ? never : P
}[keyof T]
复制代码

示例

type RequiredProps = {
  req: number
  reqUndef: number | undefined
  opt?: string
  optUndef?: number | undefined
}

type RequiredKeysResult = RequiredKeys<RequiredProps> //"req" | "reqUndef"
复制代码

RequiredKeys 中用到了 Pick,首先说下 Pick 是干吗的

Pick 是 Typescript 内置的泛型函数,接受两个 T, U,第一个参数 T 是一个对象类型,第二个参数 U 是联合类型,而且 U extends keyof T。Pick 用于过滤掉泛型 T 中不能兼容 U 的 key。

例如:

type Props = {
  req: number
  reqUndef: number | undefined
  opt?: string
  optUndef?: number | undefined
}
type result = Pick<Props, 'req' | 'opt'> // {req: number,opt?: string}
复制代码

回到 RequiredKeys 类型函数上,在遍历泛型 T 的 key 过程当中,借用空对象{}去 extends 处理过的 key(此时是一个只包含 key 的对象),若当前 key 是可选的,那么必然是兼容的,不是咱们想要的返回 never,不然是必选的,返回当前 key。

OptionalKeys

OptionalKeys 用于获取对象类型上全部可选的 key。

实现

export type OptionalKeys<T extends object> = {
  [P in keyof T]-?: {} extends Pick<T, P> ? P : never
}[keyof T]
复制代码

示例

type RequiredProps = {
  req: number
  reqUndef: number | undefined
  opt?: string
  optUndef?: number | undefined
}
type OptionalKeysResult = OptionalKeys<RequiredProps> // "opt" | "optUndef"
复制代码

OptionalKeys 的实现方式和 RequiredKeys 基本相同,区别在于条件类型的取值是至关的,具体细节能够看下 RequiredKeys 的实现分析。

PickByValue

在解读 RequiredKeys 类型函数的时候咱们说到了 Pick 这个内置类型函数,它是根据 key 来过滤对象的 key 的,而 PickByValue 则是根据 value 的类型来过滤对象的 key。

实现

export type PickByValue<T, K> = Pick<
  T,
  {
    [P in keyof T]-?: T[P] extends K ? P : never
  }[keyof T]
>
复制代码

示例

type PickByValueProps = {
  req: number
  reqUndef: number | undefined
  opt?: string
}

type PickByValueResult = PickByValue<PickByValueProps, number> //{req: number; reqUndef: number | undefined; }
复制代码

咱们来经过结果来反推一下 PickByValue,就这个示例而言,首先咱们想要的结果是过滤掉全部值类型可兼容 number 的 key,由于是过滤,因此 PickByValue 的最外层就必然要用 Pick 来作。

type PickByValue<T, K> = Pick<T, ...>
复制代码

因此目前要实现这个函数只须要搞定第二个参数就能够了。由于第二个参数必然是 keyof T 的子集,因此咱们要作就是经过 value 的类型来推出可兼容 value 类型的 key。下一步就必然要遍历 key,而且经过{}[keyof T]来获取最终的子集。

type PickByValue<T, K> = Pick<T, {
  [P in keyof T]: ...
}[keyof T]>
复制代码

在遍历过程当中判断T[P]的类型是否兼容 K 就能够了,最终结果就是实现的样子。

PickByValueExact

PickByValueExactPickByValue 的严格版

实现

export type PickByValueExact<T, ValueType> = Pick<
  T,
  {
    [Key in keyof T]-?: [ValueType] extends [T[Key]]
      ? [T[Key]] extends [ValueType]
        ? Key
        : never
      : never
  }[keyof T]
>
复制代码

源码里面是双向 extends,感受使用 IfEquals 更严格一些。

export type PickByValueExact<T, K> = Pick<
  T,
  {
    [P in keyof T]-?: IfEquals<[K], [T[P]], P>
  }[keyof T]
>
复制代码

示例

type PickByValueProps = {
  req: number
  reqUndef: number | string
  opt?: string
}

type PickByValueExactResult = PickByValueExact<PickByValueProps, number> //{req: number;}
复制代码

实现思路与 PickByValue 大体相同,区别就是判断的地方,PickByValueExact 使用 IfEquals 作严格匹配。

Omit

Omit 的做用就是反向 Pick,删除泛型 A 中可匹配泛型 B 的 key。

实现

export type Omit<A, B extends keyof A> = Pick<A, Exclude<keyof A, B>>
复制代码
type OmitProps = {
  name: string
  age: number
  visible: boolean
  sex: string | number
}

// {
// name: string;
// visible: boolean;
// sex: string | number;
// }
type OmitResult = Omit<OmitProps, 'age'>
复制代码

反向 Pick 能够借助 Pick 来作,只要对 Pick 的第二个参数作处理便可。方式就是使用 Exclude 泛型函数对 keyof A、B 取补集,获取到泛型对象 A 中过滤掉兼容泛型 B。

OmitByValue

反向 PickByValuePickByValue 是只包含,OmitByValue 是只过滤。

实现

export type OmitByValue<T, U> = Pick<
  T,
  {
    [P in keyof T]: T[P] extends U ? never : P
  }
>
复制代码

示例

type OmitProps = {
  name: string
  age: number
  visible: boolean
  sex: string | number
}
// {
// age: number;
// visible: boolean;
// sex: string | number;
// }
type OmitByValueResult = OmitByValue<OmitProps, string>
复制代码

PickByValue 相似,只是将 extends 的结果交换了位置,就能够实现反向操做,具体思路请看 PickByValue 的分析。

OmitByValueExact

实现

export type OmitByValueExact<T, ValueType> = Pick<
  T,
  {
    [Key in keyof T]-?: [ValueType] extends [T[Key]]
      ? [T[Key]] extends [ValueType]
        ? never
        : Key
      : Key
  }[keyof T]
>
复制代码

源码里使用双向 extends 判断两个类型是否严格兼容,我这里用 IfEquals 函数搞了一下。

export type OmitByValueExact<A, B> = Pick<
  A,
  {
    [P in keyof A]-?: IfEquals<A[P], B, never, P>
  }[keyof A]
>
复制代码

示例

type OmitProps = {
  name: string
  age: number
  visible: boolean
  sex: string | number
}
// {
// name: string
// age: number
// visible: boolean
// }
type OmitByValueExactResult = OmitByValueExact<OmitProps, string | number>
复制代码

相信看过以前的套路,聪明的你必定能想到 OmitByValueExact 的实现方式是和 PickByValueExact 的实现方式相似的,区别在于 IfEquals 类型函数结果返回值交换了位置,具体思路请看 PickByValueExact 的实现思路。

Intersection

Intersection 用于获取对象类型 key 的交集。

实现

export type Intersection<T extends object, U extends object> = Pick<
  T,
  Extract<keyof T, keyof U> & Extract<keyof U, keyof T>
>
复制代码

示例

type IntersectionProps = {
  name: string
  age: number
  visible: boolean
  value: number
}
type DefaultProps = { age: number; value: number }
// {
// age: number;
// value: number;
// }
type IntersectionResult = Intersection<IntersectionProps, DefaultProps>
复制代码

Intersection 类型函数接受<A,B>两个对象类型,最终获得的是两个对象类型 key 的交集在 A 上的 Pick。 因此咱们只要先解两个对象类型 key 的交集,而后再对 A 进行 Pick 就 ok 了。

求交集可使用 Extract 泛型函数,将 A、B 使用索引操做符将 key 转为联合类型,而后使用 Extract 求两个联合类型的交集,最后对 A 进行 Pick 便可。

我的认为第二个 Extract 是没有必要的由于对两个联合类型求交集,谁先谁后两个结果都是同样的。

Diff

Diff 类型函数接受两个泛型变量 T、U,且 T、U 都是对象类型,用于获取泛型 U 在泛型 T 上的补集。

实现

export type Diff<T extends object, U extends object> = Pick<
  T,
  Exclude<keyof T, keyof U>
>
复制代码

示例

type Props = {
  name: string
  age: number
  visible: boolean
  value: number
}
type Props2 = { age: number; value: number }
// {
// name: string;
// visible: boolean;
// }
type DiffResult = Diff<Props, Props2>
复制代码

通过上面类型函数中对 Pick 函数的应用,咱们应该已经知道 Pick 是用来处理对象类型,并返回对象类型的子集,所以求补集就应该从两个对象类型的 key 下手。开始已经提到 Exclude 用于求两个联合类型的补集,所以就能够经过索引类型修饰符获取到两个对象类型的 key 的联合类型,而后再经过 Exclude 取补集,最后经过 Pick 取 T 的子集便可。

Overwrite

Overwrite 接收两个泛型参数 T、U,且都为对象类型,做用是若 U 中属性在 T 中也存在,则覆盖 T 中的属性。

实现

export type Overwrite<
  T extends object,
  U extends Object,
  I = Diff<T, U> & Intersection<U, T>
> = Pick<I, keyof I>
复制代码

示例

type Props1 = { name: string; age: number; visible: boolean }
type Props2 = { age: string; other: string }

// {
// name: string
// age: string
// visible: boolean
// }
type OverwriteResult = Overwrite<Props1, Props2>
复制代码

若是对 DiffIntersection 这两个泛型函数了解的话,那么 Overwrite 就小菜一碟了。咱们知道 Diff 用于获取两个泛型参数的补集,Intersection 用于获取两个泛型参数的交集,最后合成交叉类型便可。

你可能会疑问,结果直接Diff<T, U> & Intersection<U, T>就能够了,为何还要使用 Pick 多一次遍历呢?

咱们分别用两种状况看一下类型推断结果。

  1. 使用 Pick
type OverwriteResult = Overwrite<Props1, Props2>
// =>
// {
// name: string
// age: string
// visible: boolean
// }
复制代码
  1. 不使用 Pick
export type Overwrite<T extends object, U extends Object> = Diff<T, U> &
  Intersection<U, T>
type OverwriteResult = Overwrite<Props1, Props2>
// => Pick<OverwriteProps, "name" | "visible"> & Pick<NewProps, "age">
复制代码

能够看出不使用 Pick 的结果对于用户是不友好的,没法直接从 IDE 中看到类型推断的结果。

Assign

AssignOverwrite 的能力更强大一些。它接收两个泛型参数 T、U,且都为对象类型,做用是若 U 中的属性在 T 中存在则覆盖,不存在则添加。

实现

export type Assign<
  T extends object,
  U extends object,
  I = Diff<T, U> & Intersection<U, T> & Diff<U, T>
> = Pick<I, keyof I>
复制代码

示例

type Props1 = { name: string; age: number; visible: boolean }
type Props2 = { age: string; other: string }
// {
// name: string;
// age: string;
// visible: boolean;
// other: string;
// }
type AssignResult = Assign<Props1, Props2>
复制代码

Assign 在实现上与 Overwrite 区别是在处理 I 上比 Overwrite 多&了Diff<U, T>Overwrite 的做用是覆盖已有元素,那么实现 Assign 只须要将在 T 上不存在的属性合并到 T 上就 ok 了,所以就可使用Diff<U, T>的方式获取到在 U 上而再也不 T 上的属性,最后与前面和为交叉类型。

Unionize

Unionize 接收一个泛型参数,且为对象类型,做用是将对象类型转为单独 key 对象的联合类型。

实现

export type Unionize<T extends object> = {
  [P in keyof T]: { [Q in P]: T[P] }
}[keyof T]
复制代码

示例

type Props = { name: string; age: number; visible: boolean }
// {
// name: string;
// } | {
// age: number;
// } | {
// visible: boolean;
// }
type UnionizeResult = Unionize<Props>
复制代码

起初看到这个是懵逼的,而后仔细想一下,发现已经写过不少这种方式了,直接遍历对象 key,而后将 value 构形成对象,最后在经过索引操做符取全部值的联合类型就能够了。

PromiseType

PromiseType 用于获取 Promise 的泛型类型。

实现

export type PromiseType<T extends Promise<unknown>> = T extends Promise<infer V>
  ? V
  : never
复制代码

示例

// string
type PromiseTypeResult = PromiseType<Promise<string>>
复制代码

PromiseType 中用到了 infer,infer 的做用是在条件类型中作延时推断,infer 用到绝佳能够实现强大的功能。

PromiseType 将泛型 T extends Promise,并在 Promise 泛型类型使用 infer 推断其类型,若 T 为 Promise 类型,则 V 就是 Promise 的泛型类型,不然为 never。

思考一下,若是深度解析 Promise 泛型呢? 🤔

DeepReadonly

utility-typesDeepX递归类型基本上相同,X的逻辑在上面已经分析过了,主要分析是 Deep 逻辑。

实现

export type DeepReadonly<T> = T extends ((...args: any[]) => any) | Primitive
  ? T
  : T extends _DeepReadonlyArray<infer U>
  ? _DeepReadonlyArray<U>
  : T extends _DeepReadonlyObject<infer V>
  ? _DeepReadonlyObject<V>
  : T
export interface _DeepReadonlyArray<T> extends ReadonlyArray<DeepReadonly<T>> {}
export type _DeepReadonlyObject<T> = {
  readonly [P in keyof T]: DeepReadonly<T[P]>
}
复制代码

示例

type Props = {
  first?: {
    second?: {
      name?: string
    }
  }
}
type DeepReadonlyResult = DeepReadonly<Props>
复制代码

源码中分别对数组和对象类型作了处理,能够看到_DeepReadonlyObject泛型函数在遍历 T 的过程当中再次调用DeepReadonly进行递归解析。

思考一下,为何没有循环引用呢? 🤔

Optional

Optional 接收两个泛型参数 T、K,且 T 为对象类型,K 为 T 全部 key 联合类型的子集,做用是 T 中可兼容 K 的属性转换为可选的,默认是所有。

实现

export type Optional<
  T extends object,
  K extends keyof T = keyof T,
  I = Omit<T, K> & Partial<Pick<T, K>>
> = Pick<I, keyof I>
复制代码

示例

type Props = {
  first: string
  second: number
  third: boolean
}
// {
// first?: string
// second?: number
// third: boolean
// }
type OptionsalResult = Optional<Props, 'first' | 'second'>
复制代码

咱们能够先想一下,要怎么作才能实现这样的功能。

既然要处理部分属性,因此咱们能够先将这部分属性删除,等处理好了以后再合并过来,没错,源码就是这么干的。

若是你是按照顺序读下来的,确定已经 Omit、Pick 这两个泛型函数的做用了(Omit 只删除、Pick 只保留,忘了的话能够翻上去看看),所以咱们就能够先使用 Omit 将将要处理的属性先删除,而后使用 Pick 只保留将要处理的属性并使用 Partial 泛型函数处理,最后再使用交叉类型将两者合并起来。

ValuesType

ValuesType 接收一个泛型参数,能够是数组或对象,用于获取值的联合类型。数组在这里较多的指元组,由于普通数组全部元素的类型相同,就不必联合了。

实现

export type ValuesType<
  T extends Array<any> | ReadonlyArray<any> | ArrayLike<any> | object
> = T extends Array<any> | ReadonlyArray<any> | ArrayLike<any>
  ? T[number]
  : T extends object
  ? T[keyof T]
  : never
复制代码

示例

type Props = {
  first: string
  second: number
  third: boolean
}
// string | number | boolean
type ValuesTypeResult = ValuesType<Props>
复制代码

ValuesType 处理参数主要分为两部分:对数组的处理和对对象的处理。对数组的处理使用T[number]很是优雅,而且是元组类型转联合类型最简单的方式;对对象的处理用的就比较多了,使用索引操做符就能够了。

ArgumentsRequired

ArgumentsRequiredOptional 相似,用于将对象的某些属性变成必选的

实现

export type ArgumentsRequired<
  T extends object,
  K extends keyof T = keyof T,
  I = Omit<T, K> & Required<Pick<T, K>>
> = Pick<I, keyof I>
复制代码

示例

type Props = {
  name?: string
  age?: number
  visible?: boolean
}
// {
// name: string
// age: number
// visible: boolean
// }
type ArgumentsRequiredResult = ArgumentsRequired<Props>
复制代码

实现方式的解析能够看 Optional,这里就很少说了。

TupleToUnion

ValuesType 中已经提到一个特别简单的方式。还有一种方式也值得学习一下。

在类型系统中,元组类型是兼容数组类型的。

// 'true'
type ret = [number, string] extends Array<any> ? 'true' : 'false'
复制代码

所以就可使用 infer 来推断出数组的泛型类型。

实现

export type TupleToUnion<T extends any[]> = T extends Array<infer U> ? U : never
复制代码

示例

// string | number
type TupleToUnionResult = TupleToUnion<[string, number]>
复制代码

UnionToIntersection

UnionToIntersection 用于将联合类型转为交叉类型

实现

export type UnionToIntersection<T> = (T extends any
? (arg: T) => void
: never) extends (arg: infer V) => void
  ? V
  : never
复制代码

示例

type UnionToIntersectionResult = UnionToIntersection<
  { name: string } | { age: number } | { visible: boolean }
>
复制代码

UnionToIntersection 这个泛型函数仍是要好好理解的,这里用到了 TypeScript 类型系统中的概念,同一类型变量的多个候选类型将会被推断为交叉类型,这是 TS 类型系统函数参数位置逆变的知识。逆变与协变这篇文章说的很清晰,能够深刻了解一下。

了解了TS类型系统后,UnionToIntersection就比较好了解了。已知泛型函数接受的是一个联合类型,经过分布式条件类型构建同一类型变量多个候选类型,而后再使用延时推断获取到 V 的类型。

总结

解读utility-types中的高级类型,我发现 TypeScript 远远不止咱们在函数参数位置赋予一个类型那么简单,必定要善用 TypeScript 类型推断的能力,有时候你会发现,让一个函数具有良好的类型推断能力写的类型居然比运行代码还长,为了让代码更稳定,更能被同事理解,有时咱们也必须这样作😭,别让你的 TypeScript 成为 AnyScript

相关文章
相关标签/搜索