更强大的使用TypeScript(2)

147 阅读6分钟
    /**
        @date 2021-12-25
        @description ts-type-challenge-中等题目1-18(除17题)
    */

壹(序)

接着第一期的更强大的使用TypeScript(1),每天坚持做一道题,两周过去了,完成了十七道题目,做完或者理解了一道题目就往草稿箱中累加,到今天已经做了十七道题以及我个人的解析;

总的来说不是一个简单的过程,经常需要下班回家才能做,做完已经十一二点了,不过我觉得我的努力是值得的;

最后,圣诞快乐🎄!

贰(Get Return Type)

需要获取函数返回的类型,那么使用 infer 推断出返回的类型即可,还需要接收传参为 any[]

type MyReturnType<T> = T extends (...args: any[]) => infer P ? P : never;

叁(Omit)

一开始想法是这样:

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

但是 never 依然是会传出来的,所以需要使用到 Exclude 去除掉 K 中包含的属性名:

type MyOmit<T, K> = {
  [P in Exclude<keyof T, K>]: T[P]
}

肆(Readonly 2)

从标题不好理解需要做什么,所以简述一下:实现一个 Readonly 2,在 Readonly 的基础上,可以传第二个参数来选择将哪些属性设置为 readonly;

首先回顾一下 Readonly 的实现:

type MyReadonly<T> = {
    readonly [P in keyof T]: T[P];
}

Readonly2 在以上的基础上增加了一个泛型 K,那么我们需要确定 K 的范围,是 Tkey 组成的,想要只将 K 中的属性设置为 readonly 的话,则直接遍历 K

type MyReadonly2<T, K extends keyof T> = {
  readonly [P in K]: T[P]
}

这样得到了需要设置为 readonly 的属性,然后使用 & 符号去处理其他不需要 readonly 的属性,想到上一个题,是实现 Omit,是剔除掉不需要的属性,那么正好可以使用在这:

type MyReadonly2<T, K extends keyof T> = {
  readonly [P in K]: T[P]
} & {
  [U in Exclude<keyof T, K>]: T[U]
}

最后需要兼容第二个参数不传值,表示所有属性都设置为 readonly 的情况,所以给 K 设置默认值:

type MyReadonly2<T, K extends keyof T = keyof T> = {
  readonly [P in K]: T[P]
} & {
  [U in Exclude<keyof T, K>]: T[U]
}

伍(DeepReadonly)

深层次的 Readonly,肯定是需要使用递归的,判断是否是 object,是则需要递归,不是直接进行 Readonly 操作,所以得到:

type DeepReadonly<T> = {
  readonly [P in keyof T]: T[P] extends object ? DeepReadonly<T[P]> : T[P]
}

但是函数也是 object,所以需要对函数做一下处理:

type DeepReadonly<T> = {
  readonly [P in keyof T]: T[P] extends (...args: any) => any ? T[P] : T[P] extends object ? DeepReadonly<T[P]> : T[P]
}

陆(TupleToUnion)

将元组转为联合类型,需要确保传入的范型 T 是数组类型的,然后根绝映射操作,直接返回每一项即可:

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

柒(Chainable Options)

想要实现链式类型,我们必须在内部再次使用此类型,所以需要一个传入的泛型,而且是内部自己解析的,所以给一个泛型 T 且初始为空对象,用于保存最后 get 函数得到的类型,所以对于 get 直接返回自己定义的 T 即可;

对于函数 option 就需要返回自身类型 Chainable,而且还需要传入 T,为了最后 get 得到完整的类型,对 option 返回的 Chainable 需要传入 T & {...},后面的对象就是 option 接收的东西,所以对于 option 也需要传入泛型,一个是 key 的泛型 K extends string,保证是 string,一个是 value 的泛型 V,然后构造 option 自己的类型 Chainable,传入 T & { [key in K]: V }

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

捌(Last of Array)

获取数组最后一项,首先保证传入的 T 是一个数组,所以 T extends any[],然后使用 infer 推断出最后一项即可:

type Last<T extends any[]> = T extends [...args: any[], L: infer P] ? P : T

玖(Pop)

与上面的 Last of Array 类似,想要获取除最后一项的前面部分,使用 infer 推断出前面部分即可:

type Pop<T extends any[]> = T extends [...other: infer Item, L: any] ? Item : never

拾(PromiseAll)

首先传入的 values 可以是常数,可以是Promise,所以可以给 values 的类型定义为 any[],但是后面需要用到 values 里面的值,所以定义一个泛型用来与 values 对应,但是不需要外面传入,那么自定义一个泛型 Tvalues 就是这个 T

declare function PromiseAll<T extends any[]>(values: T): any

不过在全是常数的情况下,上面接收 values 是不对的,需要加上 readonly

declare function PromiseAll<T extends any[]>(values: readonly [...T]): any

最后是处理返回值,肯定返回的是 Promise,然后在这个返回的 Promise 中做处理,由于传入的可能是一个 Promise,所以需要做一个判断,并且将传入的 Promise 的类型推断出来:

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

拾壹(Type Lookup)

查找类型中是否有 type 为指定参数的类型,可以直接判断 U 是否 extends { type: T }

type LookUp<U, T> = U extends { type: T } ? U : never

拾贰(TrimLeft)

去除左边空格或空行,需要递归处理,判断传入的泛型S是否extends ${(' ' | '/n' | '/t')}${infer P},是则继续处理,不是则表示处理完成,直接返回:

type TrimLeft<S extends string> = S extends `${(' ' | '\n' | '\t')}${infer P}` ? TrimLeft<P> : S

拾叁(Trim)

实现两边的去空格,可以先实现TrimLeft和TrimRight,然后结合使用,TrimRight与TrimLeft类似:

type TrimRight<S extends string> = S extends `${infer P}${(' ' | '\n' | '\t')}` ? Trim<P> : S
type TrimLeft<S extends string> = S extends `${(' ' | '\n' | '\t')}${infer P}` ? Trim<P> : S
type Trim<S extends string> = TrimLeft<TrimRight<S>>

拾肆(Capitalize)

实现首字母大写,肯定是需要把首字母和剩余部分单独拎出来的,所以可以使用 infer 进行推断,首位可以用 S[0] 得到,就能先推断出剩余部分 L,然后再利用推断出来的 L 推断出首位 F,再使用模板字符类型返回结果:

type Capitalize<S extends string> = S extends `${S[0]}${infer L}`
  ? S extends `${infer F}${L}`
  ? `${Uppercase<F>}${L}`
  : ''
  : ''

不过感觉这样比较有臃肿,用了两个条件判断,其实可以一次性推断出 FL,再直接返回:

type Capitalize<S extends string> = S extends `${infer F}${infer L}` ? `${Uppercase<F>}${L}` : S

拾伍(Replace)

Replace也需要通过 infer 推断出需要替换的部分,然后进行替换即可:

type Replace<S extends string, From extends string, To extends string> = S extends `${infer F}${From}${infer L}` ? `${F}${To}${L}` : S

不过还需要处理 From 为空字符串的情况:

type Replace<S extends string, From extends string, To extends string> = From extends ''
  ? S
  : S extends `${infer F}${From}${infer L}`
  ? `${F}${To}${L}`
  : S

拾陆(ReplaceAll)

ReplaceAllReplace 类似,不同之处在于 ReplaceAll 还需要处理后面的,所以需要递归一下:

type ReplaceAll<S extends string, From extends string, To extends string> = From extends ''
  ? S
  : S extends `${infer F}${From}${infer L}`
  ? `${F}${To}${ReplaceAll<L, From, To>}`
  : S

拾柒(Append Argument)

想要往函数的入参追加一个参数,必须得到函数目前的入参,然后追加上即可,如何得到目前的入参呢?使用 infer

type AppendArgument<Fn, A> = Fn extends (...args: infer T) => infer R ? (...args: [...T, A]) => R : never 

拾捌(Length of String)

ts是不能直接从 string 中取 length 属性的,所以想到可以声明一个 L 表示 length,给默认值0,然后用 inferS 推断成两个片段,一个是首位 Start,一个是剩余的 Last,然后递归处理,将 Last 作为 新的S 传入,而 L 由于减少了 Start 这一位,所以 加1,跳出递归就是 S 为空字符串时,直接返回 L 即可,不过ts中不能加1, 所以需要声明一个array,然后取这个array的length

type LengthOfString<S extends string, L extends any[] = []> = S extends ''
  ? L['length']
  : S extends `${infer Start}${infer Last}`
  ? LengthOfString<Last, [Start, ...L]>