持续创作,加速成长!这是我参与「掘金日新计划 · 6 月更文挑战」的第12天,点击查看活动详情
前言
在学习typescript的过程当中,有一个github库对其类型的学习特别有帮助,是一个有点类似于leetcode的刷题项目,能够在里面刷各种关于typescript类型的题目,在上一篇文章中,我们完成了中等的第五题,今天来做中等的第六题 12-medium-chainable-options
下面这个是类型体操github仓库:
12-medium-chainable-options
今天要实现的工具类型可能比较难理解,但是一定要先理解完题目的意思和要求,再去研究怎么解答或者查找题解,不然是完全看不明白的。
import type { Alike, Expect } from '@type-challenges/utils'
declare const a: Chainable
const result1 = a
.option('foo', 123)
.option('bar', { value: 'Hello World' })
.option('name', 'type-challenges')
.get()
const result2 = a
.option('name', 'another name')
// @ts-expect-error
.option('name', 'last name')
.get()
type cases = [
Expect<Alike<typeof result1, Expected1>>,
Expect<Alike<typeof result2, Expected2>>,
]
type Expected1 = {
foo: number
bar: {
value: string
}
name: string
}
type Expected2 = {
name: string
}
首先要先来理解一下什么是 可串联构造器,他就是一个可以连续调用的对象,并且最后在调用了get以后会返回一个对象类型。
所有的 option 函数都是在为最后的这个对象添加属性,而 get 方法就会返回最后的这个对象类型。
利用 JS 进行对比学习
这道题比较特殊,跟之前碰到过的工具类型都比较不同,能够通过不断地调用一个option方法来为一个对象添加属性,并且在调用get方法之后返回这个对象。
那么首先我们先定义一个对象a用来保存option方法和get方法,并且需要在a内部定义一个对象用来存储option定义出来的键值,还有一个option方法能够添加键值,为了能够递归调用,那么option方法在最后一定得返回a,再接着还需要一个get方法返回a中定义的对象。
let a = {
Obj:{},
option:function(key,val){
if(typeof val != 'string'){
this.Obj[key] = this.option(val)
return a
}else{
this.Obj[key] = val
return a
}
},
get:function(){return this.Obj}
}
这就是 JS 版本大概的实现,能先通过 JS 来大概体会一下,题目要我们实现的是一个什么样的对象类型。
实现 Chainable
通过 JS 实现我们能大概看出来,这个对象中应该会有两个函数,第一个函数会一直给一个对象添加属性,另一个函数则是在调用的时候,返回这个对象,那么最开始的思路就有了,我们需要创建一个对象类型,这个对象类型内部有两个方法,第一个方法会不断地为一个对象添加属性,另一个的返回值就是这个对象,那么这个定义在 a 中的对象,我们能够通过 泛型来进行保存,并且付给它一个默认值也就是空对象 = {}
type Chainable<T extends object = {}> = {
option():void
get(): T
}
然后我们就需要在 option 中去为这个对象添加属性,这里可以使用 Record 工具类型,传入键值,就会为我们返回一个对象类型。
type A6 = Record<'bar', { value: 'Hello World' }>
// type A6 = {
// bar: {
// value: 'Hello World';
// };
// }
并且在 JS 代码中我们能够得到,option 这个方法,需要在返回 a 这个类型,并且因为我们是把返回对象通过泛型参数传入的,所以可以通过交叉类型来把旧的对象和新的option生成的对象类型做一个合并在作为参数返回。
这样我们就能够得到最后的答案了:
type Chainable<T extends object = {}> = {
option<K extends string, V>(key: K, value: V): Chainable<T & Record<K, V>>
get(): T
}
但是这时候会发现,测试中又有一个经典的老朋友报错了
这说明,这行注释下面的代码应该要报错,不然注释就会报错,下面这行代码能够看出来,option 重复传入了两个同样的键,那么我们就需要对传入的 Key 做一个判断,看看是否存在原本的对象当中,存在的话就要赋值为 never 产品报错。
type Chainable<T extends object = {}> = {
option<K extends string, V>(key: K extends keyof T ? never : K, value: V): Chainable<T & Record<K, V>>
get(): T
}
这样到这里,测试用例就能够全部通过了。
知识点
关于上述提到了部分的知识点:
- Record 工具类型
- never 的使用
- 递归
never 代表永远不存在的值,能够赋值给一个变量这样在这个变量被赋值的时候就会产生报错,用于定义在代码中本应该不会运行到的部分
或者 never 还能够用来去除对象中的键,这点在映射类型的重映射中经常使用,可以看看之前的文章:
总结
今天我们做完了中等的第六题,题目的展现方式很不一样,但是实用性还是特别高的,特别是在定义一些我们一开始没办法保证的对象类型,这算是一种很不错的实践。