likes
comments
collection
share

教科书式的 TypeScript 类型体操工具——utility-types

作者站长头像
站长
· 阅读数 57

最近一段时间比较忙,好久没有写博客了,正好这段时间跟 TypeScript “杠上”了,刚好又看到一个 github 上的仓库 utility-types,写了很多有意思的类型工具,不过这个仓库代码比较老了,最高支持 TypeScript v3.7,所以在新项目的实战中就不太建议直接使用了,不过其源码还是有很多很有价值的东西,所以我想在此进行一些源码分析、学习,以提高自己的 TypeScript 水平。

utility-types 代码比较老了,有一部分它实现的工具类型后来都成为了 TypeScript 的内置工具类型。

Aliases & Type Guards 别名和类型守卫

源码地址 https://github.com/piotrwitek/utility-types/blob/master/src/aliases-and-guards.ts

Primitive

就是定义了几个原始类型:

export type Primitive =
  | string
  | number
  | bigint
  | boolean
  | symbol
  | null
  | undefined;

isPrimitive

原始类型的类型守卫:

// 源码 : val is Primitive 建议删掉
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;
  }
};

Falsy

定义了做条件判断时都为 false 的类型:

export type Falsy = false | '' | 0 | null | undefined;

isFalsy

Falsy 类型的类型守卫:

// 源码的 : val is Falsy 建议删掉
export const isFalsy = (val: unknown): val is Falsy => !val;

Nullish

定义了空类型:

export type Nullish = null | undefined;

isNullish

空类型的类型守卫:

// 源码的 : val is Nullish 建议删掉
export const isNullish = (val: unknown): val is Nullish => val == null;

Union operators 类型的联合操作

源码地址 https://github.com/piotrwitek/utility-types/blob/master/src/mapped-types.ts

SetIntersection

获取 A、B 两个类型的交叉类型,和内置的 Extract 一样。

export type SetIntersection<A, B> = A extends B ? A : never;

SetDifference

获取 A 类型中有,但 B 类型中没有的类型,与内置的 Exclude 一样。

export type SetDifference<A, B> = A extends B ? never : A;

SetComplement

与 SetDifference 作用一样,但是对后面一个类型增加了一个约束,这个约束即前面那个类型。

export type SetComplement<A, A1 extends A> = SetDifference<A, A1>;

SymmetricDifference

获取 A、B 类型中没有的交叉的类型。它与 SetDifference 的不同点在于,SetDifference 只能获取到 A 类型不交叉于 B 类型的部分,而 SymmetricDifference 同时也能获取 B 类型不交叉于 A 类型的部分。

export type SymmetricDifference<A, B> = SetDifference<A | B, A & B>;

NonUndefined

排除一个类型中的 undefined 类型,内置有一个 NonNullable,排除 null 和 undefined 类型

export type NonUndefined<A> = A extends undefined ? never : A;

Object operators 对象类型的操作

源码地址 https://github.com/piotrwitek/utility-types/blob/master/src/mapped-types.ts

FunctionKeys

取出一个对象中值为函数类型的键,返回格式是 'key1' | 'key2' ......

export type FunctionKeys<T extends object> = {
  [K in keyof T]-?: NonUndefined<T[K]> extends Function ? K : never;
}[keyof T];
  • T extends object 就是约束我们传入的泛型都必须是对象类型的
  • [K in keyof T]-? 是遍历取出对象的键,如果该键是可选属性会被转换成必选属性,这个键用 K 表示
  • NonUndefined<T[K]> 把取出来的值类型做一个 undefined 过滤
  • extends Function ? K : never 判断值类型是否为函数类型,如果为函数类型则返回这个键,如果不是则返回 never
  • [keyof T] 是把前面的结果都取出来,当然 never 的会被忽略掉

NonFunctionKeys

取出一个对象中值不为函数类型的键,返回格式同上。

export type NonFunctionKeys<T extends object> = {
  [K in keyof T]-?: NonUndefined<T[K]> extends Function ? never : K;
}[keyof T];

和前面那个很像只是 extends Function ? never : K 改变了一下顺序。

MutableKeys、WritableKeys、ReadonlyKeys

取出对象中可变、可写、只读的成员名,返回格式是 'key1' | 'key2' ......

export type MutableKeys<T extends object> = {
  [P in keyof T]-?: IfEquals<
    { [Q in P]: T[P] },
    { -readonly [Q in P]: T[P] },
    P
  >;
}[keyof T];

export type WritableKeys<T extends object> = MutableKeys<T>;

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 IfEquals<X, Y, A = X, B = never> = (<T>() => T extends X ? 1 : 2) extends <
  T
>() => T extends Y ? 1 : 2
  ? A
  : B;

这段源码最难的就是要看懂 IfEquals,我们先改变一下其书写方式:

type IfEquals<X, Y, A = X, B = never> =
  (<T>() => T extends X ? 1 : 2) extends 
  (<T>() => T extends Y ? 1 : 2)
  ? A : B;

这样就清晰多了。

<T>() => T extends X ? 1 : 2 这个表达式其实在部分情况下它就写个 X 也是可以的,为了帮助理解,我先简化一下:

type IfEquals<X, Y, A = X, B = never> = X extends Y ? A : B;

不过在某些情况下这个判断会有问题,比如:X= { readonly x: string } Y = { x: string }X = { x: never }X = { x: string; y: number } Y = { x: string } 等情况的结果也不对。

所以 IfEquals 的作用就是判断 X、Y 两个类型是否相等的,相等返回 A,不相等返回 B。

看懂了 IfEquals 再来看 MutableKeys 就容易了,其实就是对对象类型的每个成员进行遍历:

  • { [Q in P]: T[P] } 原封不动遍历
  • { -readonly [Q in P]: T[P] } 去掉 readonly 约束
  • 如果一样就返回键名,不一样则返回 never

ReadonlyKeys 只是在返回的时候稍微不一样,如果一样就返回 never,不一样则返回键名

RequiredKeys、OptionalKeys

取出必选、可选的成员名,返回格式是 'key1' | 'key2' ......

export type RequiredKeys<T> = {
  [K in keyof T]-?: {} extends Pick<T, K> ? never : K;
}[keyof T];

export type OptionalKeys<T> = {
  [K in keyof T]-?: {} extends Pick<T, K> ? K : never;
}[keyof T];
  • {} extends Pick<T, K> 如果 K 是一个可选成员,那么它便不能形成一个有效的约束,所以一个空对象也会被判断为是满足条件的

PickByValue

通过值类型来 Pick 成员

export type PickByValue<T, ValueType> = Pick<
  T,
  { [Key in keyof T]-?: T[Key] extends ValueType ? Key : never }[keyof T]
>;
  • T[Key] extends ValueType ? Key : never 把匹配 ValueType 类型的 Key 取出来

PickByValueExact

通过值类型来精确Pick成员,与 PickByValue 相比 PickByValueExact 的类型匹配更加精确。

比如:类型 number | undefined,PickByValue 可以匹配 number 类型和 number | undefined 类型,而 PickByValueExact 只能匹配到 number | undefined 类型

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

还是先改一下源代码的格式:

[ValueType] extends [T[Key]] ? ([T[Key]] extends [ValueType] ? Key : never) : never;
  • [T[Key]] extends [ValueType] ? Key : never 把匹配 ValueType 类型的 Key 取出来(非精确匹配)
  • [ValueType] extends [T[Key]] 这里是反过来就行了一个匹配,这样就可以过滤掉上面的例子中 number 类型和 number | undefined 类型的情况,因为反过来就匹配不了

OmitByValue

通过值类型来Omit成员

export type OmitByValue<T, ValueType> = Pick<
  T,
  { [Key in keyof T]-?: T[Key] extends ValueType ? never : Key }[keyof T]
>;

OmitByValueExact

通过值类型来精确Omit成员

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

Intersection

提取两个对象有交叉的成员

export type Intersection<T extends object, U extends object> = Pick<
  T,
  Extract<keyof T, keyof U> & Extract<keyof U, keyof T>
>;
  • Extract<keyof T, keyof U> & Extract<keyof U, keyof T> 从 T 的 Key 中提取 U 的 Key 也有的 Key,再从 U 的 Key 中提取 K 的 Key 也有的 Key,然后再取个交集。最后把这个几个交集里面的 Key 对应的成员从 T 中取出来。

Diff

提取两个对象中前面这个对象存在但后面这个对象不存在的成员

export type Diff<T extends object, U extends object> = Pick<
  T,
  SetDifference<keyof T, keyof U>
>;
// 等价
export type Diff<T extends object, U extends object> = Pick<
  T,
  Exclude<keyof T, keyof U>
>;

Subtract

从 T 类型中减去 T1 类型,其中T1 类型是个对象类型,T 类型满足 T1 类型约束,SubtractDiff 很相似,只是后者多了传入的两个对象之间的约束关系

export type Subtract<T extends T1, T1 extends object> = Pick<
  T,
  SetComplement<keyof T, keyof T1>
>;

Overwrite

将U 类型中与 T 类型键名相同的成员进行覆写,返回覆写后的 T类型。可以传三参数,但是传第三个参数的时候前两个参数就失效了,所以用的时候传两参数即可。

export type Overwrite<
  T extends object,
  U extends object,
  I = Diff<T, U> & Intersection<U, T>
> = Pick<I, keyof I>;
  • I = Diff<T, U> & Intersection<U, T> 取出 T 中存在但是 U 中不存在成员,然后以 U 类型为主,取出 T、U 类型的交集,最后把他们并到一起

Assign

像合并两个对象一样,将 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>;
  • I = Diff<T, U> & Intersection<U, T> & Diff<U, T> 取出 T 中存在但是 U 中不存在成员,然后以 U 类型为主,取出 T、U 类型的交集,再取出 U 中存在但是 T 中不存在成员,最后把他们并到一起

DeepReadonly

就是字面意思:深只读,就是递归将每一层的成员都变成只读类型

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;
/** @private */
// tslint:disable-next-line:class-name
export interface _DeepReadonlyArray<T> extends ReadonlyArray<DeepReadonly<T>> {}
/** @private */
export type _DeepReadonlyObject<T> = {
  readonly [P in keyof T]: DeepReadonly<T[P]>;
};

这里使用了递归,直接看让人很不好理解,我把上面的代码拆解成一段意思相近的伪代码,帮助理解:

function DeepReadonly(T) {
  // 如果是函数或原始类型
  if (T === ((...args: any[]) => any) | Primitive) {
    return T;
  }
  if (T !== isDeepReadonlyArray(T)) {
    return DeepReadonlyArray(T);
  }
  if (T !== isDeepReadonlyObject(T)) {
    return DeepReadonlyObject(T);
  }
  return T;
}

function DeepReadonlyArray(T) {
  // 数组处理得到 U
  // ...
  return DeepReadonly(U)
}

function DeepReadonlyObject(T) {
  // 对象处理得到 V
  // ...
  return DeepReadonly(V)
}

DeepRequired

深必选所有成员,原理与 DeepReadonly 类似

export type DeepRequired<T> = T extends (...args: any[]) => any
  ? T
  : T extends any[]
  ? _DeepRequiredArray<T[number]>
  : T extends object
  ? _DeepRequiredObject<T>
  : T;
/** @private */
// tslint:disable-next-line:class-name
export interface _DeepRequiredArray<T>
  extends Array<DeepRequired<NonUndefined<T>>> {}
/** @private */
export type _DeepRequiredObject<T> = {
  [P in keyof T]-?: DeepRequired<NonUndefined<T[P]>>;
};

DeepNonNullable

深去空,原理与 DeepReadonly 类似

export type DeepNonNullable<T> = T extends (...args: any[]) => any
  ? T
  : T extends any[]
  ? _DeepNonNullableArray<T[number]>
  : T extends object
  ? _DeepNonNullableObject<T>
  : T;
/** @private */
// tslint:disable-next-line:class-name
export interface _DeepNonNullableArray<T>
  extends Array<DeepNonNullable<NonNullable<T>>> {}
/** @private */
export type _DeepNonNullableObject<T> = {
  [P in keyof T]-?: DeepNonNullable<NonNullable<T[P]>>;
};

DeepPartial

深可选所有成员,原理与 DeepReadonly 类似

export type DeepPartial<T> = T extends Function
  ? T
  : T extends Array<infer U>
  ? _DeepPartialArray<U>
  : T extends object
  ? _DeepPartialObject<T>
  : T | undefined;
/** @private */
// tslint:disable-next-line:class-name
export interface _DeepPartialArray<T> extends Array<DeepPartial<T>> {}
/** @private */
export type _DeepPartialObject<T> = { [P in keyof T]?: DeepPartial<T[P]> };

Optional

将 T 对象的 K 键变成可选成员,如果省略 K 就是把 T 对象的所有成员变成可选

export type Optional<T extends object, K extends keyof T = keyof T> = Omit<
  T,
  K
> &
  Partial<Pick<T, K>>;
  • Omit<T, K> 排除匹配 K 键名的成员
  • Partial<Pick<T, K>> 把 K 键名的成员提取出来,并都变成可选
  • 把上面的两个结果合并起来

ValuesType

把数组、类数组、对象的成员的类型都取出来

export 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;

如果是数组、类数组就取出数组的元素类型,如果是对象就取出对象成员的键的联合类型

Mutable、Writable

去掉成员的 readonly ,让其变成一个可变或者叫可写成员

export type Mutable<T> = { -readonly [P in keyof T]: T[P] };
export type Writable<T> = Mutable<T>;

AugmentedRequired

将指定成员变成必选属性

export type AugmentedRequired<
  T extends object,
  K extends keyof T = keyof T
> = Omit<T, K> & Required<Pick<T, K>>;
  • Optional 原来类似

Special operators 特殊的类型操作

源码地址 https://github.com/piotrwitek/utility-types/blob/master/src/mapped-types.ts

Unionize

将对象类型的成员全员取出来,返回其每个成员组成的联合类型

export type Unionize<T extends object> = {
  [P in keyof T]: { [Q in P]: T[P] };
}[keyof T];

PromiseType

取出 Promise 包裹的返回类型

export type PromiseType<T extends Promise<any>> = T extends Promise<infer U>
  ? U
  : never;
  • Promise<infer U> 可以自行推断出 Promise 包裹的返回类型并赋值给 U

Brand

我也没体会到它的作用是啥😅

export type Brand<T, U> = T & { __brand: U };

UnionToIntersection

联合类型转交叉类型,如 T1 | T2 -> T1 & T2

export type UnionToIntersection<U> = (U extends any
? (k: U) => void
: never) extends (k: infer I) => void
  ? I
  : never;
  • 第一步 (U extends any ? (k: U) => void : never) 这里主要是把传入的 U 类型(联合类型)转化成 (T1 extends any ? (k: T1) => void : never) | (T2 extends any ? (k: T2)=> void : never) | ...
  • 第二步 (k: T1) => void | (k: T2) => void extends ((k: infer I) => void) 利用 infer 的特性同一类型变量的多个候选类型将会被推断为交叉类型

Flow's Utility Types

源码地址 https://github.com/piotrwitek/utility-types/blob/master/src/utility-types.ts

$Keys

keyof 作用一样,但是增加了 object 约束

export type $Keys<T extends object> = keyof T;

$Values

取出对象的值的所有类型

export type $Values<T extends object> = T[keyof T];

$ReadOnly

把一个对象类型的成员(包括其深层的成员)都变成只读类型

export type $ReadOnly<T extends object> = DeepReadonly<T>;

$Diff

Subtract

export type $Diff<T extends U, U extends object> = Pick<
  T,
  SetComplement<keyof T, keyof U>
>;

$PropertyType

获取给定的属性类型

export type $PropertyType<T extends object, K extends keyof T> = T[K];

$ElementType

通过键名或键类型获取数组、元祖或者对象对应成员的类型

export type $ElementType<
  T extends { [P in K & any]: any },
  K extends keyof T | number
> = T[K];

$Call

等价内置的 ReturnType

export type $Call<Fn extends (...args: any[]) => any> = Fn extends (
  arg: any
) => infer RT
  ? RT
  : never;

$Shape

把一对象类型的成员都变成可选成员

export type $Shape<T extends object> = Partial<T>;

$NonMaybeType

等价 NonNullable,排除 null 与 undefined

export type $NonMaybeType<T> = NonNullable<T>;

Class

获取 T 类型的构造函数类型

export type Class<T> = new (...args: any[]) => T;

mixed

export type mixed = unknown;

延伸阅读

github.com/type-challe…

www.typescriptlang.org/docs/handbo…

转载自:https://juejin.cn/post/7142657346781577246
评论
请登录