写在前面
使用仓库:type challenges
类型体操是什么
类型体操是一种调侃的说法,其来源于TypeScript中各种复杂的类型操作。
打个比方,你可能已经了解到,TypeScript中内置了很多工具类型,
比如Partial
:
interface Foo {
a: number
b: number
}
type PartialFoo = Partial<Foo> // { a?: number, b?: number}
该工具可以将一个类型中的成员全部变为可选的,它的实现是这样的:
type Partial<T> = {
[P in keyof T]?: T[p]
}
再来一个例子,Exclude
:
type Foo = Exclude<1 | 2, 1 | 3> // 2
它的作用是从类型T中排除掉所有包含的U属性,它的实现如下:
type Exclude<T, U> = T extends U ? never : T
这两个是非常基础的,其余还有几十种内置工具类型,这里就不一一列举了。
总之,这些自定义的类型和其实现手段,我们可以统称为类型体操。
为什么要学习类型体操
既然TS内置了那么多工具类型,为什么我们还需要学习呢?
因为它们是远远不够的,尤其是对于开发一个库或者封装一个组件的场景而言。
以下图片来自于我随手截的vue仓库的一段类型代码。
这种代码在基于TS的库中数不胜数,如果你想要给你的用户更好的类型提示、约束,你就必须定义很多自定义的工具类型。
所以学习类型体操的目的就是:为了让我们的TS能力更进一步,也为了我们封装的组件/库更加强大。
Type Challenges
我们这个系列的学习方式将围绕这Type Challenges
仓库来进行。
这个开源项目旨在帮开发者通过练习的方式逐步掌握TS,在这里有着各路神仙提交的题目和解答,每一道题都是一个你需要实现的工具类型,题目还按照难度和tag进行了分类:
可以说是非常友好了。
事实上,TS的类型系统本身是图灵完备的,所以你完全可以将它看做另一门纯函数式语言。
那么,掌握一种语言最快的方式,就是大量的练习。
让我们开始吧!
环境搭建
我们可以建一个自己的仓库专门用来练习Type Challenges。
每个将要实现的类型都是一个独立的ts文件。
而类型的测试也十分简单,在Type Challenges
仓库中随便点开一道题目,比如我们点开Pick:
然后将它的代码复制到我们自己的Pick.ts文件中:
Pick.ts
:
interface Todo {
title: string
description: string
completed: boolean
}
type TodoPreview = MyPick<Todo, 'title' | 'completed'>
const todo: TodoPreview = {
title: 'Clean room',
completed: false,
}
此时,如果你的TS编译器没出问题,那么你就可以看到报错:
我们就把这个报错当成测试不通过的标志吧!你可以多设置几个测试用例。
那么,就让我们以没有报错为目标来完成我们的题目吧~!
下面来和大家一起完成我们的第一道题。
第一道题:Pick
题目要求大家已经在上面看到了,下面直接开始实现。
首先回顾一下用法,MyPick
类型接受两个泛型参数T和K,最终构建一个新的类型,新类型的成员为T中的K。其中,K是一个联合类型(union type)。
对比学习法
如果你是新手,强烈建议可以先用JS来做一遍,再根据JS翻译为对应的TS(前面说过,TS类型本身是图灵完备的)。
JS实现:
// 我们用function来模拟类型工具,用对象来模拟类型本身
function MyPick(obj, keys) {
//第一步 创建一个新的类型
const result = {}
//第二步 遍历keys,将todo里面的每一个key和值都copy给obj
keys.forEach((key) => {
// 注意 key可能不在原来的todo里面,所以要进行判断
if(key in obj){
result[key] = obj[key]
}
})
//第三步 返回
return result
}
可以看到,JS版本就已经实现了,接下来就是将它翻译为TS就可以了,我们来总结一下需要用到的语法:
- 返回一个类型
- 遍历一个联合类型( union type )
- 取值( result[key] )
- 判断一个成员是否在类型中( if (key in obj) )
下面来将它们一一转换成TS写法
返回一个类型
在TS中,返回一个类型很简单,直接写就可以了:
type MyPick = {
}
遍历一个联合类型
在TS中,遍历一个类型的key(Union Type),我们可以直接使用in
操作符,in的左边是每次循环接受的变量,右边是要遍历的类型,整个式子右边就是每次循环要进行的操作了:
[key in T] :
而对于一个接口类型,我们需要先获取它的key组成的联合类型,再进行遍历:
[key in keyof T]
取值
和JS一样,TS的取值也很简单:
T[K]
判断一个成员是否在类型中
在JS中,我们通过判断key在todo中,才进行赋值操作。
而在TS类型中,我们可以把这种手段理解为约束。
在这道题里,我们就需要给第二个泛型参数加上约束:它们的key必须存在于T中。
我们自然而然就想到了extends
:
type MyPick<T, K extends T> = {
}
然而这样写会报错,因为T是一个接口,而K是一个联合类型,我们其实只想要接口中的键组成的联合类型,所以应该再加上keyof
关键字:
type MyPick<T, K extends keyof T> = {
}
这样,约束就完成了。当用户传入的K包含T中没有的键时,就会看到错误。
最终,我们的答案:
type MyPick<T, k extends keyof T> = {
[key in T] : T[key]
}