TS类型体操 之 中级类型体操挑战收官之战
# 中级类型体操挑战收官之战
之前在写入门前置知识的时候就提到过 “有几道题目没解出来”,其实这 2 道题目和后面 “困难” 的部分题目题型很像
所以今天理解透这 2 道题目后中级类型应该就过关了,顺便还能为“困难”打下基础
这 2 个题目我个人总结就是 “无中生有”,不存在的参数,就自己创建一个
# 热身 - 3196 · Flip Arguments
在讲上面 2 个题目时想先讲一下 ,一个 获取方法内参数,并且将他们反转
的一个题目(之前好像也没机会讲)
这个题目其实就是一个简单的 extends
的应用,难在一个小小的思维转变
需求如下:获取一个方法内的参数,并且将他们反转过来
type cases = [
Expect<Equal<FlipArguments<() => boolean>, () => boolean>>,
Expect<Equal<FlipArguments<(foo: string) => number>, (foo: string) => number>>,
Expect<Equal<FlipArguments<(arg0: string, arg1: number, arg2: boolean) => void>, (arg0: boolean, arg1: number, arg2: string) => void>>,
]
2
3
4
5
在这个题目,需要理解几个点
- extends 其实就是一个包含关系,包括
方法
也可以 extends - 说到反转内容,数组是最容易反转的,03192 就是
Reverse
的题目了 - 方法中的参数,基于 ES6 的知识,结合
...
就能形成数组了,和 2 相呼应
所以解题重点就在于 extends
答案如下:
// Reverse 是 03192 题目已经做出来的,这里就直接复用不重复写了
type FlipArguments<T> = T extends (..._: infer args) => infer R ? (...arg0: Reverse<args>) => R : T
2
- infer 是计算,是占位,之前有讲过,
infer R
就是帮返回值占个位 ..._: infer args
这一块则是整个题目的灵魂所在,这里我们不能用infer
占位,只能用一个变量来占(这个变量起什么名字都行),然后他的类型才是infer args
这样的话,args 则代替了输入的参数的全部内容了- 包括最终返回的时候
(...arg0: Reverse<args>)
这个表达式中,args0
并没有任何实际意义,也是一个参数占位而已
讲这个题目主要是为了 做个铺垫,因为 Promise.all 也有获取参数的场景
# TS 类型体操遇上 declare
declare
是一个声明,可以看到 TS 的源码中对 JS 的方法很多都用了声明,这也是为什么我们使用 parseInt
能有类型检查的原因。declare
就不在这里展开,姑且理解为 为一个方法/函数约定参数和返回值
讲题 : 00020-medium-promise-all (opens new window)
要求:键入函数 PromiseAll,它接受 PromiseLike 对象数组,返回值应为 Promise<T>
,其中 T 是解析的结果数组。
答案初始模版:
declare function PromiseAll(values: any): any
本以为只是一个简单的获取 values
然后包裹一层 Promise
的操作,可是由于各种语法问题,PromiseAll 本身就是一个方法而不是泛型参数,所以上面讲的套路行不通了(注意区分和对比)。
扣一下今天的主题无中生有 :
- 在之前的题目中,比如我们要用到计数器,我们会新建一个
C extends unknown[] = []
。 - 需要新的变量都会新建一个变量并且赋一个默认值去用,这也算无中生有
Q:思考一下在
declare
中如果想无中生有的话,要加那里?
A:方法想无中生有,加泛型!比如下面这样的
declare function PromiseAll<T>(values: any): any
在回到题目,返回值应为 Promise<T>,其中 T 是解析的结果数组。
- 返回值是 Promise
- Promise 里面的泛型类型 T 是一个数组(这里的 T,也是无中生有生出来的,不然原题模版哪里来的 T),所以我们直接约束 T 为一个数组
declare function PromiseAll<T extends any[]>(values: any): Promise<T>
// 测试用例
type TestPromise = typeof promiseAllTest1 // 这时候得到的是 Promise<any[]>
2
3
4
到这一步就已经成功了一半,返回 Promise,并且泛型 T 是数组,剩下的一半就是把 T 转换为具体的数组,而不是 any[]
错误示范,错误示范,错误示范
// 报错
// 'T' is declared but its value is never read.
// 'infer' declarations are only permitted in the 'extends' clause of a conditional type.
declare function PromiseAll<T extends any[]>(values: infer T): Promise<T>
// 或 错误示范2:
// 不报错可是也没效果
declare function PromiseAll<T extends any[]>(values: T): Promise<T>
2
3
4
5
6
7
8
- 错误 1 意思是 infer 只能在
extends
表达式里面去占位,普通情况下不行 - 错误示范 2 中,因为 T 本来就是
any[]
,values 也确实是数组,没毛病,可是也推导不出来结果
正确 25% 的答案:
declare function PromiseAll<T extends any[]>(values: [...T]): Promise<T>
利用错误示范 2 中的原理,反推 T,values 是数组,而我们要做的是获取这个数组里面的内容, 如果我们把 T 分散了([...T]
)这个类型依旧没报错的话,T 就和 values 完全相等了,这时候返回 T,测试用例第一个例子就 pass 了
这时候测试用例是过了,可是 3,4 行代码类型检查不过。
as const
这个也在 前置知识里面提到过,as const 的会把所有的值拿出来,而且变成 readonly。因为 const 确实是只读的标记。
正确 50% 的答案:
declare function PromiseAll<T extends any[]>(values: readonly [...T]): Promise<T>
加上 readonly 后,声明的等式成立了,就还差 2 个测试用例的情况,因为他们传入的数值里面包含 Promise.resolve
这种情况。我们需要从 Promise.resolve 中把参数取出来,那么就要从返回值 Promise<T>
去入手了
Promise<T> 注意这里的 <T>
已经是 泛型 了,又回到熟悉的 type 体操的感觉了,这时候题目就变成了
正确 100% 的答案(复杂版):
declare function PromiseAll<T extends any[]>(values: readonly [...T]): Promise<PromiseRes<T>>
type PromiseRes<T extends any[], R extends any[] = []> = T extends [infer F, ...infer Rest] ? PromiseRes<Rest, [...R, F extends Promise<infer A> ? A : F]> : R
2
3
新建了一个 PromiseRes 为了处理 T 这个数组,F extends Promise<infer A> ? A : F
就是为了判断是不是 Promise 类型的,是的话把 A 提出来,最后存到一个数组里面返回。用例通过,木的问题
正确 100% 的答案(简单版):
declare function PromiseAll<T extends any[]>(values: readonly [...T]): Promise<{ [P in keyof T]: T[P] extends Promise<infer R> ? R : T[P] }>
这里有 2 个小知识点稍微在拓展下
# Promise<{[P in keyof]}> 里面为什么可以这样写
说再多还不如写段代码
// 写法1.
setTimeout(function () {
console.log('定时器')
}, 1000)
// 写法2
var log = function () {
console.log('定时器')
}
setTimeout(log, 1000)
2
3
4
5
6
7
8
9
10
2 种写法最后运行效果一模一样,同理,上面简单版写法还能写成这样的:
declare function PromiseAll<T extends any[]>(values: readonly [...T]): Promise<PromiseRes<T>>
type PromiseRes<T> = { [P in keyof T]: T[P] extends Promise<infer R> ? R : T[P] }
2
3
# {[P in keyof T]}
这不是对象的写法吗,为什么在这里最终会返回数组类型?
从 Pick 方法开始学体操的时候 {[P in keyof T]}
确实返回的都是对象类型,毕竟 {}
在那里摆着
不过凡事都有例外,因为 T
是个数组类型,而 keyof T
得到的其实是 0,1,2,3... 数组长度的索引,和数组特有的属性方法
。所以 P 对应的也是 0,1,2,3... + 特有的属性和方法
比如这个例子中
var aKeys:ArrKeys = ''
type Arr = ['Jioho', 'Promise']
type ArrKeys = keyof Arr
2
3
根据智能提示看到 ArrKeys 的取值范围全是数组的方法和属性
所以说,{[P in keyof T]}
的返回值还得看 T 到底是什么类型
# 12 · 可串联构造器
这一题的难度在用这是一个链式调用,而且还得把之前的记录动态累计下来
const result1 = a.option('foo', 123).option('bar', { value: 'Hello World' }).option('name', 'type-challenges').get()
之前接触的题目都是传入数据,得出结果。而且这个题目和 Promise.all 一样,没有给出很多的初始泛型,这就又得靠我们自己的 无中生有 技巧
解题的思路上面也有说了
- 一个是调用 option 时 键值对 动态累计下来
- 题目给给出一个
get
来获取结果
突破点首先在 get
,因为这才是获取结果的位置,假设我们返回 T
,T 包含了所有的键值对内容。
按作用域来看,T 肯定是作为累计
的变量,所以 T 作用域应该在 get
和 option
之上,第一步的无中生有就给 Chainable
加一个泛型,然后记得补上默认值(根据返回结果,返回的应该是个对象)
第一步思考结果如下:
type Chainable<T = {}> = {
option(key: string, value: any): any
get(): T
}
2
3
4
第二步:链式调用和变量累计?
链式调用其实核心原理就是把 this
把当前对象作为调用的返回值。
Chainable(a)含有 option 方法,调用 option 后在返回一个 Chainable(a),这时候返回值就又能继续调用了
做过前面题目的其实都应该知道,想做到变量累积,那必须是递归(比如计数器的累积),不断的调用自身,并且不断追加新参数进去
结合上面说的 2 点:递归,返回自己,追加参数,得出下面的答案
这是有点错误的示范:
// 这是 527 题目的答案,为Object追加新的键值对。复用上了
type AppendToObject<T, U extends string, V> = { [P in keyof T | U]: P extends keyof T ? T[P] : V }
type Chainable<T = {}> = {
// 错误地方: AppendToObject<T,key, value>
option(key: string, value: any): Chainable<AppendToObject<T,key, value>>
get(): T
}
2
3
4
5
6
7
8
以上的结果思路是对的,不过就是注释的地方有点问题 AppendToObject<T,key, value>
这时候的 key 和 value 还是 JS 变量,而不是 TS 的内容。
这时候可能就会想到 typeof
不是可以把 JS 的内容转换为 TS 吗?
转是可以转,不过转了之后返回值是 stirng,而不是我们想要的 123
var b = '123'
type Test = typeof b // string
2
到这一步,应该要想起 Promise.all
题目讲到的 变量倒推,复习下 Promise.all
declare function PromiseAll<T extends any[]>(values: readonly [...T]): ...
定义了泛型 T,直接 readonly [...T]
放入 values 中进行一个类似倒推的操作。对于链式调用来说同理
type AppendToObject<T, U extends string, V> = { [P in keyof T | U]: P extends keyof T ? T[P] : V }
type Chainable<T = {}> = {
option<K extends string,V>(key: K, value: V): Chainable<AppendToObject<T,K,V>>
get(): T
}
2
3
4
5
6
定义K
和V
,这 2 个类型放入参数中,进行一个倒推,用 K 来代表 key 的值,V 代表 value 的值,在结合 AppendToObject
,就可以为 T 动态添加参数了
与此同时 option 的返回值返回的则是最新的 Chainable
和当前链式调用后最全的 T,这时候在调用 get 就能把累积的变量都拿出来了
# 总结
这几个题目给了非常好的提示性效果,没有参数要学会自己创造参数,学会无中生有
如果能用 infer 就尽量用好 infer,不过 infer 只能在 extends 相关的表达式里面去用
像 Promise.all 和链式调用这种函数/方法里面用不了的,就用一个新变量进行一个类型的倒推,把对应的值反过来约束为对应的泛型
尤其是最后一题,一定要好好理解,因为我去探过路了,困难题 中会遇到很多这样的情况,需要学会 无中生有 和 和并对象
至此中等题目就刷完了,我觉得比较有用的技巧也总结了好几篇笔记,欢迎到我的 TS 专栏翻一翻,点个赞支持下。祝你们也刷题愉快,困难题见!