/**
@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
的范围,是 T
的 key
组成的,想要只将 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
对应,但是不需要外面传入,那么自定义一个泛型 T
,values
就是这个 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}`
: ''
: ''
不过感觉这样比较有臃肿,用了两个条件判断,其实可以一次性推断出 F
和 L
,再直接返回:
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)
ReplaceAll
与 Replace
类似,不同之处在于 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,然后用 infer
将 S
推断成两个片段,一个是首位 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]>