Typescript 类型挑战

type-challenges 是 GitHub 上的一个项目,对于想通过 Typescript 的特性写出符合自己需要的复杂类型的同学是一个非常好的练手机会。

这篇日志里,我将不定时更新自己的解答,并会附上相关文档地址。

Omit

在线地址

实现 TS 内置的 Omit

内置实现方式:

type Omit<T, K extends keyof any> = Pick<T, Exclude<keyof T, K>>;

又是 Pick 又是 Exclude,咱就不解释了,自己看文档。

我的实现:

type MyOmit<T, K extends keyof any> = {
  [key in keyof T as key extends K ? never : key]: T[key]
}

使用了 TS 4.1 引入的新语法 as clause。其作用在于对给定的 key (例中为 key in keyof T 后产生的 key)继续 map 为其他 key(这里根据条件 map 为 neverkey)。

当 map 为 never 时,意味着删除对应的 key,也就达到了我们 omit 的目的。

as clause, 配合 Template Literal Types,可以很方便的基于一个类型得到另一个类型。

Readonly2

在线地址

区别于内置 Readonly

实现一个通用MyReadonly2<T, K>,它带有两种类型的参数T和K。
K指定应设置为Readonly的T的属性集。如果未提供K,则应使所有属性都变为只读,就像普通的Readonly一样。

我的实现:

type MyReadonly2<T, K extends keyof any = any> = 
{
  -readonly [key in keyof T as key extends K ? never : key]: T[key]; 
} & {
  readonly [key in keyof T as key extends K ? key : never]: T[key]; 
}

as clause 就不解释了,这里的作用是筛选出所有符合条件的属性加上 readOnly,而对于不符合条件的则去除readonly, 这里用到了 mapped type modifiers 来实现。

深度 Readonly

在线地址

提到深度,必然就联想到递归了

实现一个通用的DeepReadonly<T>,它将对象的每个参数及其子对象递归地设为只读。

递归要考虑的就是递归停止条件,这个问题里面,停止的条件就是值不再是对象

type DeepReadonly<T extends Record<string, any>> = {
  readonly [key in keyof T]: T[key] extends Record<string, any> ? 
    T[key] extends Function ? T[key] : DeepReadonly<T[key]> : T[key]
}

这里针对测试用例加了对函数的判断,实际上,数组及其他对象也需要处理,这样看来,我这个答案其实并不是太好,找到一个比较完美的解决方案

type DeepReadonly<T> = keyof T extends never
  ? T
  : { readonly [k in keyof T]: DeepReadonly<T[k]> };

Tuple to Union

在线地址

实现泛型TupleToUnion<T>,将tuple的成员转成union,如:

type Arr = ['1', '2', '3']

const a: TupleToUnion<Arr> // expected to be '1' | '2' | '3'

我的实现:

type TupleToUnion<T extends any[]> = T[number];

属于比较常见的 tupleunion 方式,另外推荐一个别人的法子:

export type TupleToUnion<T> = T extends Array<infer ITEMS> ? ITEMS : never

Union to Tuple

这个是我在实际开发中的需求,搜了下,实现还挺复杂的,这里记录一个相对简短的实现:

type UnionToTuple<T> = (
    (
        (
            T extends any
                ? (t: T) => T
                : never
        ) extends infer U
            ? (U extends any
                ? (u: U) => any
                : never
            ) extends (v: infer V) => any
                ? V
                : never
            : never
    ) extends (_: any) => infer W
        ? [...UnionToTuple<Exclude<T, W>>, W]
        : []
);

这段代码的解读可参考:https://github.com/microsoft/TypeScript/issues/13298#issuecomment-724542300

Chainable Options

在线地址
实现类型 Chainable,满足:

declare const config: Chainable

const result = config
  .option('foo', 123)
  .option('name', 'type-challenges')
  .option('bar', { value: 'Hello World' })
  .get()

// expect the type of result to be:
interface Result {
  foo: number
  name: string
  bar: {
    value: string
  }
}   

以下为他人实现方式:

type Chainable<T={}> = {
  option<K extends string, V>(key: K, value: V): Chainable<T & { [key in K]: V}>
  get(): T
}

首先,要满足链式,需要有两个条件:
1. 能够将前面的类型传递到后面,所以需要定义为泛型type Chainable<T={}> = xxxx
2. option 依然需要返回 Chainable

而合并之前的类型与当前的 key value则用 union 即可。

Last of Array

在线地址

Implement a generic Last that takes an Array T and returns it’s last element’s type.

For example

type arr1 = ['a', 'b', 'c']
type arr2 = [3, 2, 1]

type tail1 = Last<arr1> // expected to be 'c'
type tail2 = Last<arr2> // expected to be 1

如果熟悉 infer 关键字,这个题就非常简单了:

type Last<T extends any[]> = T extends [...infer _, infer L] ? L : never;

其他解答:

type Last<T extends any[]> = [any, ...T][T["length"]];

既然不能像 js 那样运算 arr[arr.length – 1], 那就在首位添加一个元素然后 afterUnshiftArr[arr['length']]

POP

与此类似题

Implement a generic Pop that takes an Array T and returns an Array without it’s last element.

For example:

type arr1 = ['a', 'b', 'c', 'd']
type arr2 = [3, 2, 1]

type re1 = Pop<arr1> // expected to be ['a', 'b', 'c']
type re2 = Pop<arr2> // expected to be [3, 2]

PromiseAll

在线地址

Type the function PromiseAll that accepts an array of PromiseLike objects, the returning value should be Promise where T is the resolved result array.

For example

    const promise1 = Promise.resolve(3);
const promise2 = 42;
const promise3 = new Promise<string>((resolve, reject) => {
  setTimeout(resolve, 100, 'foo');
});

// expected to be `Promise<[number, number, string]>`
const p = Promise.all([promise1, promise2, promise3] as const)

别人的解答:

declare function PromiseAll<T extends any[]>(values: readonly [...T]): Promise<{
  [k in keyof T]: T[k] extends Promise<infer V> ? V : T[k];
}>

涨姿势点:{[k in keyof T]: T[k]} 如果 T 为 Tuple,那么得到的结果也是 Tuple 而非 Map