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