TS 一些工具泛型的使用及其实现(续一)

2,342 阅读7分钟

之前写了一篇 TS 一些工具泛型的使用及其实现, 但是一直没怎么使用 TS,回首看文章,发现自己都看不懂了。 期间内 TS 也有一些变化,所以这一篇将会承接上篇文章,分析解读更多的工具泛型,主要来自 utility-types项目的源码。 阅读本流水账需要对 TS 中的以下东西有所了解

  • extends
  • keyof
  • in
  • infer
  • &
  • |
  • ?
  • -?
  • +?
  • never
  • unkown
  • any
  • readonly
  • void

正文

ArrayElement

提取数组成员类型, 一个思路是 用 extends 限制数组类型, 然后用数组 key 类型为 number 的特性取出其属性类型

type ArrayElement<T extends readonly unknown[]> = T[number];

第二种写法的核心思路就是用 infer 来隐射 数组的属性类型

type ArrayElement<A> = A extends readonly (infer T)[] ? T : never

Exclude & Extract vs. Diff & Filter

TS 内置类型定义涵盖了 Exclude & Extract, 但是在它的官方文档又给出了另外的名字

type Diff<T, U> = T extends U ? never : T;
type Filter<T, U> = T extends U ? T : never;

就类型定义的代码而言,Exclude === Diff, Extract === Filter,蜜汁操作

NonNullable

从类型 T 中排除 null 和 undefined

type NonNullable<T> = Exclude<T, null | undefined>

Parameters

拿到函数的参数类型,不定参数的组织形式就是一个数组,参考 ArrayElement的第二种写法,利用infer去取到类型

type Parameters<T extends (...args: any) => any> = T extends (...args: infer P) =>  any ? P : never

ConstructorParameters

要拿到构造函数参数的类型,参考 Parameters,加上 new 即可

type ConstructorParameters<T extends new (...args: any) => any> = T extends new (...args: infer P) =>  any ? P : never

InstanceType

获取实例类型,跟 ParametersConstructorParameters 差不多,不过这次不 infer 参数了,而是 infer 函数返回数据

type InstanceType<T extends new (...args: any) => any> = T extends new (...args: any) =>  infer R ? R : never

NonFunctionKeys

拿到对象中所有非函数类型属性的 key,比如

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

NonFunction

如果要把对象中的函数剔除,留下其他的话,我们只要把 NonFunctionKeys 的结果再 Pick 一下就好了。

type NonFunction<T extends object> = Pick<T,NonFunctionKeys<T>>

PickByValue

不再局限于属性值为函数,根据给定的类型进行挑选对象成员,比如我只想要一个对象中属性值类型为 number | string的成员。

type PickByValue<U, T> = Pick<U, {[K in keyof U]-?: U[K] extends T ? K : never}[keyof U]>

但是其实你在给 T 传入类似 number | string 这样的类型其实是有些暧昧的,number | string 是代表 number | string 这个类型本身,又或者可以包含它的所有子类型呢,即 number 类型行,string 类型也接受,any, never 来者不拒, 显然这儿 PickByValue 是可以 PickT 的子类型。

OmitByValue

有了 PickByValue,怎么能没有 OmitByValue

type OmitByValue<U, T> = Pick<U, {[K in keyof U]: U[K] extends T ? K : never}[keyof U]>

于是你就会发现,上面的 NonFunction, 用 OmitByValue 有了更加简单的写法

type NonFunction<T> = OmitByValue<T, Function>

PickByValueExact

我们需要一个 PickByValueExact 来进行精确的 Pick,只选中类型 T 本身,忽略其子类型。 所以我们判断一个类型是不是其类型本身。

A extends B  -> A <= B
B extends A  -> B >= A
// 根据我多年前的数学经验,满足 A extends B && B extends A 就说明 A == B 为同一类型(误)
// (其实只是可以互相 assign 额)

于是

type Same<A, B, X = 1, Y = 0> = A extends B ? B extends A ? X : Y : Y;

// 实验一下
type K = Same<number | string, string> // -> 0 | 1

发现情况有点不对,K 的类型是 0 | 1,跟预期的有点不一致。条件类型( T extends U ? X : Y)在 T 为联合类型(例如 A | B)的时候会自动分发类型,(A extends U ? X : Y) | (B extends U ? X : Y) 于是

type K = Same<number | string, string> // 相当于展开成下面的

type L = (number extends string ? string extends number ? 1 : 0 : 0) | (string extends string ? string extends string ? 1 : 0 : 0) // -> 0 | 1

kk, 有点烦自动分发条件类型,所幸只有对联合类型才会触发该行为,所以我们在处理的时候包一层, 把它统一塞到数组(或者转成函数)里面去,这样就可以绕过去了

type Same<A, B, X = A, Y = never> = [A] extends [B] ? [B] extends [A] ? X : Y : Y;

于是 PickByValueExact 就可以这样写了

export type PickByValueExact<T, V> = Pick<
  T,
  {
    [K in keyof T]-?: Same<T[K], V, K>
  }[keyof T]
>;

PS: Same 没法 cover 一些顶级类型(any, unkown)和可选属性的 case,其实并不是真正的同一类型

type A = Same<{ b?: string }, {}, 1, 0> // 1 -> {} == { b?: string }
type B = Same<{ a?: string }, {}, 1, 0> // 1 -> {} == { a?: string }
// 那么 { a?: string } === { b?: string } ???,然而

type C = Same<{ b?: string }, { a?: string }, 1, 0> // 0 -> {a ?: string} != {b ?: string}

// 另外 Same 对于 unkown 和 any 这两顶级类型判断是有问题的
type D = Same<{ a: any }, {  a: string }, 1, 0> // 1
type D = Same<{ a: any }, {  a: unkown }, 1, 0> // 1

OmitByValueExact

PickByValueExact,自然就有 OmitByValueExact 与之对应, 还是将错就错利用一下之前写好的 Same

type OmitByValueExact<T, V> = Omit<
  T,
  {
    [K in keyof T]-?: Same<T[K], V, K>
  }[keyof T]
>;

// 还可以用 Pick & 倒转 Same 的返回, 实现这个 OmitByValueExact
type OmitByValueExact<T, V> = Pick<
  T,
  {
    [K in keyof T]-?: Same<T[K], V, never, K>
  }[keyof T]
>;

Equals

之前泛型Same并没有办法推断出两个类型是否绝对等同,类似带有属性修饰器的类型例如 { readonly a: string } 跟 { s: string }, Same 会判断成一致。 双向 extends 的方法显然适用性有限。如何判断两个类型是否绝对一致,TS issue 区有人给出了一个比较 Hack 的解决方案

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

其核心思路是利用 conditional types(延时条件类型)依赖于内部类型一致性检查。 Equals 依赖了三个泛型参数,X, YT,即使在只传入X,Y的情况下也会根据现有的信息进行初步的类型推断,如果能推断出就返回最终的类型,推断不出最终类型就返回当前推断结果, defer 推断过程,等待新的类型参数进来。 我们可以大胆推测,在只传了 XY的情况下,ts 内部把 defer 的条件类型标注成了新的类型比如 X''Y''

type Equals<X, Y> =
    X'' extends
    Y'' ? true : false;

是否等同的判断就变成了,对 X''Y''的判断,而 X''Y''的组织形式是一致的,X'' = fn(X), 最后就变成了 XY 内部一致性的检查。最终推断出是不是真正的类型等同。 因为资料比较少,以上有部分推测,仅供参考。

RequiredKeys

通过 Omit Pick 我们可以根据对象的 key 来剔除、选择对象某些属性,通过 OmitByValue PickByValue 我们可以根据值的类型剔除、挑选某些对象属性。 那么有没有办法找到对象必填key的集合呢?的确有, 需要一个小 Trick {} extends { a ?: string} ? X : Y 一定会返回 X 类型,但是 {} extends { a : string} ? X : Y 一定会返回 Y,于是我们可以这样来取出所有的必填属性 key

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

OptionalKeys

与 RequiredKeys 相反的自然就是 OptionalKeys,交换一下 never 和 K 的位置即可

type OptionalKeys<T> = {
  [K in keyof T]-?: {} extends Pick<T, K> ? K : Never
}[keyof T]

ReadonlyKeys

之前实现的 Equals 可以让我们最大限度低判断两个类型是不是相等,差不多算是js中的 === 了,也可以用它来拿到所有 readonlykey, 在不知道一个属性修饰符是否为readonly的情况下,移除掉 readonly 之后还与之前是等同的(Equals), 那就说明其本来是带有 readonly 修饰符的。

type ReadonlyKeys<T extends object> = {
  [P in keyof T]-?: Equals<
    { [Q in P]: T[P] },
    { -readonly [Q in P]: T[P] },
    never,
    P
  >;
}[keyof T];

MutableKeys

readonly 与之相反的就是 mutable 了,MutableKeys 相比 ReadonlyKeys 只需要调整一下 Equals 的返回逻辑。

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

结尾

其实大数多未必会用到,只是训练自己对泛型的用法的熟练度,所以水了一篇文章。 utility-types 中还有一些泛型没去读,后面开始写业务啦,有机会再水一篇。

参考