前言
Typescript 逐渐成为前端工程师必备的技能,如何更好的理解 Typescript,使用它解析标注更为复杂的类型?类型体操是一个很好的选择。
如何利用 Typescript 的特性来实现一系列令人眼界大开的操作,这个库 type-challenges 将告诉你答案
在这篇文章中,我将从最基础的开始,来一步步完善我们的 Typescript 操作手段
Pick<T, K> [easy]
在这道题目中,我们将实现一个内置的泛型 Pick<T, K>,它的功能是从 T 中选出 K 中的属性,并将选中的属性返回。 问题链接
interface Todo {title: stringdescription: stringcompleted: boolean
}
type TodoPreview = MyPick<Todo, 'title' | 'completed'>
const todo: TodoPreview = {title: 'Clean room',completed: false,
}
测试用例如下
import type { Equal, Expect } from '@type-challenges/utils'
type cases = [Expect<Equal<Expected1, MyPick<Todo, 'title'>>>,Expect<Equal<Expected2, MyPick<Todo, 'title' | 'completed'>>>,// @ts-expect-errorMyPick<Todo, 'title' | 'completed' | 'invalid'>,
]
interface Todo {title: stringdescription: stringcompleted: boolean
}
interface Expected1 {title: string
}
interface Expected2 {title: stringcompleted: boolean
}
在我们未学习 TS 类型体操之前,看到这道题可能会有点蒙,我们不妨先使用 JS 的思维来分解一下这道题,先使用 JS 的代码来解决这道题。
根据题意以及测试用例,我们可以得到以下思路:
1.我们需要返回一个对象
2.我们需要遍历 K
3.如果 K 中的元素也在 T 中则将结果添加进 result
4.如果 K 中有 T 中没有的元素,需要抛出一个错误
根据以上信息我们可以很容易的写出以下代码
function myPick(todo, keys) {let result = {};keys.forEach((key) => {if (key in todo) picked[key] = todo[key]; });return result;
}
JS 版本的 Pick 已经完成,我们来一步步完成 TS 版本的 Pick
返回一个对象
在 TS 中我们想要返回一个对象,可以直接这样写
type MyPick<T, K> = {};
这样我们第一个点已经完成,接下来我们开始完成第二个点
遍历 K
在 TS 中我们想要遍历一个对象可以使用 Mapped type 来进行遍历,即
type MyPick<T, K> = {[P in K]: T[P]
};
在 TS 中,当我们想遍历一个对象的属性,我们可以使用 [P in K] 的方式来进行遍历。其中 P 代表着要遍历的属性中的一个。值得注意的是,在 in 右边的 K 必须是一个 union, 当他不是一个 union 时,我们需要使用 keyof 来将 K 转换为一个 union。
完成这步之后,我们就可以利用 T[P] 来得到该元素的类型
限制 K 的范围
完成上一步时,我们可以发现我们已经可以完成大部分测试用例,但并未对 K 的范围进行限制。这时候我们就可以利用 extends 来进行条件约束
type MyPick<T, K extends keyof T> = {[P in K]: T[P]
};
需要注意的是 K 是一个 union,keyof T 也将 T 中的属性转换为一个 union。当我们使用 extends 来进行条件约束的时候,TS 会使用 union 分发的特性自动遍历 union K 中的属性与 keyof T 中的属性进行比较。
假设 K 为 ‘title’ | ‘completed’ | ‘invalid’ ,T 为 ‘title’ | ‘completed’ | ‘description’。它的过程如下
step1:'title' extends 'title' | 'completed' | 'description' //通过
step2:'completed' extends 'title' | 'completed' | 'description' //通过
step3:'invalid' extends 'title' | 'completed' | 'description' //未通过,报错
如果比较成功则通过,失败则报错,这样我们就实现了所有的关键步骤,通过了所有的测试用例。
通过一步步的拆解,我们可以看到这道题还是很简单的。那让我们来看下一道题
Omit<T, K> [medium]
在这道题中,我们需要实现一个内置泛型 Omit<T, K>,它的功能是从 T 中删除 K 包括的元素,最后将结果以对象的形式返回,问题链接
interface Todo {title: stringdescription: stringcompleted: boolean
}
type TodoPreview = MyOmit<Todo, 'description' | 'title'>
const todo: TodoPreview = {completed: false,
}
测试用例如下:
import type { Equal, Expect } from '@type-challenges/utils'
type cases = [Expect<Equal<Expected1, MyOmit<Todo, 'description'>>>,Expect<Equal<Expected2, MyOmit<Todo, 'description' | 'completed'>>>,
]
// @ts-expect-error
type error = MyOmit<Todo, 'description' | 'invalid'>
interface Todo {title: stringdescription: stringcompleted: boolean
}
interface Expected1 {title: stringcompleted: boolean
}
interface Expected2 {title: string
}
同上一道题一样,我们先分析一下他有哪些点需要实现
1.需要返回一个对象
2.遍历 T
3.如果 T 中的元素不在 K 中,则将该元素添加到结果中
4.如果 K 中包括 T 中没有的元素,则报错
同样,我们先使用 JS 实现一边来加强我们的理解
function omit(T, K) {let resultT.foreach((item)=>{if(!K.includes(item)) {result.push(item)}})return result
}
我们来一步步实现上面总结的步骤
返回一个对象
通过 Pick 我们已经对这个很了解了
type MyOmit<T, K> = {}
遍历 T
通过上一道题,我们也可以很容易的写出一下 TS 代码
type MyOmit<T, K> = {[P in keyof T]: T[P]
}
需要注意的是,这次我们遍历的是 T 而不是 K,因为 T 是一个对象,所以我们要使用 keyof 来将他转化为一个 union
如果 T 中的元素不在 K 中,则将该元素添加到结果中
到这一步我们该怎么实现呢?我们想一下有什么方法可以使一个元素消失。对,我们可以使用 as never 来使一个元素消失。
在 TS 中如果一个 union 中的元素是一个 never 类型的,那么 TS 认为这个元素是一个空值,会返回去除这个值之后的结果。
type test<T> = {[P in keyof T]: T[P]
}
type foo = string | never
type r = test2<foo> // type a = string
所以根据以上特点,而在遍历 T 的过程中,一个 P 就是 union 中的一个元素,我们将这个元素断言成 never 那么他就会被 TS 看作是一个空的 union 元素,一个空的 union 元素自然不会被当作 key,也不会被 T[P] 所选中。这样我们就可以实现从 T 中删除一个元素的功能
type MyOmit<T, K> = {[P in keyof T as P extends K ? never : P]: T[P]
}
以上就是该功能点的全部实现,注意的是,我们根据上一题已经知道了,当 extends 两边是一个 union 时,就会触发 union 的分发特性(Distributive),P 中的元素会自动与 K 中的元素进行对比,如果对比通过了,说明该元素是要被删除的,则将该元素断言成 never,如果未通过,还按原来的结果处理
当 K 中不包括 T 中的属性报错
根据第一题,很容易的就可以实现
type MyOmit<T, K extends keyof T> = {[P in keyof T as P extends K ? never : P]: T[P]
}
至此,Pick 与 Omit 已经全部实现完成
总结
1.使用 keyof 对类型进行提取
2.使用 extends 对类型进行限制的两种方法
3.使用 T[P] 来获得类型
4.as never 来将 union 中的元素去除
5.使用 in 来进行遍历
这是我自己的练题仓库,里面会总结我在练习类型体操中遇到的相关问题以及相关知识点,如果这对你有帮助的话就给我一个 star 吧😄最后,为大家准备了一个前端资料包。包含54本,2.57G的前端相关电子书,《前端面试宝典(附答案和解析)》,难点、重点知识视频教程(全套)。
有需要的小伙伴,可以点击下方卡片领取,无偿分享