一起养成写作习惯!这是我参与「掘金日新计划 · 4 月更文挑战」的第1天,点击查看活动详情。
写在前面
本章将会介绍Array
、Tuple
相关的体操,并且着重介绍类型体操的关键成员——infer
。
本章所有题目来自type challenges。
DeepReadonly
在开始之前,先来解决一下我们上一期末尾留下的思考题:DeepReadonly
:
type X = {
x: {
a: 1
b: 'hi'
}
y: 'hey'
}
type Expected = {
readonly x: {
readonly a: 1
readonly b: 'hi'
}
readonly y: 'hey'
}
type Todo = DeepReadonly<X> // should be same as `Expected`
这道题本身可能并不难,但是解决它的思想十分重要:递归。
大部分人都知道在JS中也可以写出递归的函数:
// 经典的斐波那契数列算法
function fib(n) {
if (n === 1 || n === 2) return n - 1
return fib(n - 1) + fib(n - 2)
}
但是一般人可能想不到TS的类型系统也能这么玩。前面说过,TS的类型系统可以被看作一门独立的函数式语言,那么递归的重要性也就不言而喻。
回到这道题本身,DeepReadonly
将一个嵌套的接口类型所有属性都加上readonly
,由于我们不能提前知道嵌套的层数,所以自然而然应该去尝试递归解法:
//如果T是接口类型,则为它的所有键加上readonly,同时对每个键的值递归
//如果不是接口类型,则直接返回T本身(递归出口)
export type DeepReadonly2<T> = T extends Record<string, unknown>
? { readonly [Key in keyof T]: DeepReadonly<T[Key]> }
: T
在类型体操中,递归并不少见,所以我们需要有使用它的意识。
Array/Tuple热身题
TS中的Array
类型想必大家已经很熟悉了,而元组(Tuple)其实就是类型约束更精确的数组:
// 数组
const teacherArray: (string | number)[] = ['zina', 'girl', 18];
// 元组
const teacherTuple: [string, string, number] = ['zina', 'girl', 18];
你可能已经知道,在项目实践中,通常使用as const
将数组类型转换为元组:
const m = 100
const n = 'abc'
const arr = [m,n] as const //元组类型
接下来,我们可以去TS的标准库中查看一下Array
的定义,你看到的大致是这样的一个东西:
interface Array<T> {
length: number;
toString(): string;
toLocaleString(): string;
pop(): T | undefined;
push(...items: T[]): number;
concat(...items: ConcatArray<T>[]): T[];
concat(...items: (T | ConcatArray<T>)[]): T[];
join(separator?: string): string;
...
}
可以看到,Array
实质就是个接口,接受一个泛型参数(数组成员类型),ES提供的所有数组的成员都被TS类型化了。
接下来,我们通过Array
系列的题目来熟悉TS中的数组/元组操作。
First of Array
获取数组第一个值:
type arr1 = ['a', 'b', 'c']
type arr2 = [3, 2, 1]
type arr3 = []
type head1 = First<arr1> | First2<arr1> // expected to be 'a'
type head2 = First<arr2> | First2<arr2>// expected to be 3
type head3 = First<arr3> | First2<arr3> // expected to be never
获取数组的值写法和获取接口成员类似:
interface Person{
age: number
}
type Age = Person['age'] //number
type Zoo = ['cat','dog']
type Cat = Zoo[0] //'cat'
type Dog = Zoo[1] //'dog'
那么这道题就很简单了。但是我们需要考虑两个细节:
- 泛型参数的约束
- 数组为空的情况
对于未知成员数组,我们通常使用unknown[]
来表达,不同于any
,unknown
是类型安全的,不熟悉的同学可以从这里学习。
因为数组可能为空,所以我们需要通过length
属性来进行一个判断,如果长度为0,我们返回never
即可,否则再返回第一个值。
所以这道题最后的解答是这样的:
type First<T extends unknown[]> = T['length'] extends 0 ? never : T[0]
Tuple to Object
此题为将一个元组转换成interface,来看栗子:
const tuple = ["tesla", "model 3", "model X", "model Y"] as const
type result = TupleToObject<typeof tuple>
// expected { tesla: 'tesla', 'model 3': 'model 3', 'model X': 'model X', 'model Y': 'model Y'}
看起来很直观,就是将元组里的每项变成interface的键和值。
首先,我们接收的是一个元组,而不是数组,所以参数要进行限制。而在TS里,元组实际上就是readonly
修饰过的数组:
type TupleToObject<T extends readonly string[]> = {}
至于构建interface,相信大家都已经掌握了:
type TupleToObject<T extends readonly string[]> = {
[K in ?]:K
}
现在的重点在于如何将元组的成员提取出来,变成一个union
。
这其实是一个固定用法:使用xxx[number]
的形式可以返回数组/元组中的成员组成的union:
type A = ['a',1,'b',2,'c']
type U = A[number] // 'a' | 1 | 'b' | 2 | 'c'
那么此题的解即为:
type TupleToObject<T extends readonly string[]> = {
[K in T[number]]:K
}
Concat && Push
完成和JS数组操作concat
相同的功能:
type Result = Concat<[1], [2]> // expected to be [1, 2]
type Result2 = Concat<[], [2]> // expected to be [2]
熟悉ES6的同学肯定会立马想到延展操作符...
,好消息是它同样可以在类型体操中使用。
所以我们直接写出该题目的解:
//别忘记约束参数哦
type Concat<A1 extends unknown[], A2 extends unknown[]> = [...A1, ...A2]
同理,我们也可以轻松实现Push
、Unshift
功能:
type Push<T extends unknown[], U> = [...T, U]
type Unshift<T extends unknown[], U> = [U, ...T]
掌握的技巧越多,我们写类型也就会越来越轻松!
Pop & Shift
接下来可以尝试一下另一个老朋友,Pop
:
// Example:
type arr1 = ["a", "b", "c", "d"]
type arr2 = [3, 2, 1]
type re1 = Pop<arr1> // expected to be ['a', 'b', 'c']
type re2 = Pop<arr2> // expected to be [3, 2]
我们首先可以想到,如果数组长度是0或者1,那么直接返回空数组就行了:
type Pop<T extends unknown[]> = T['length'] extends 0 | 1 ? [] : xxx
关键是后面怎么办,怎么样把最后一个元素拿出来,好像一点头绪都没有。
目前为止我们接触到的泛型很强大,但是泛型参数T一直都作为一个整体,事实上,从类型安全的考虑,也不能允许用户描述任意的类型位置。
但是我们现在又确实遇到了这样的需求,所以我们需要一个新的TS特性来解决它。
现在,让我们请出本文要介绍的新概念,也是类型体操中最重要的操作符之一:infer
infer操作符
关于infer
,可以先参考一下官方文档。
infer的作用是推导泛型参数,不过一句话肯定理解不了,直接来看栗子:
type ReturnType<T> = T extends (...args: never[]) => infer R
? R
: never;
ReturnType
是TS内置的一个类型,作用是获得函数的返回值类型。
可以看到,我们使用extends进行判断,如果函数满足(...args:never[]) => T
这样的结构,我们就返回出T。
但是用正常写法,我们虽然知道这里的返回值是泛型,但我们缺少一个参数来使用它,所以我们需要一个新的泛型参数来进行占位,这就是infer
的作用。
同样的,如果我们要获取参数的类型,就可以这么写:
type Partmeter<T> = T extends (...args: infer R) => any ? R : never
除了函数,infer也可以在其他任何泛型位置进行占位,接下来我们会继续介绍。
关于infer
,你可能还有许多疑问,如果要深入理解,可以去看官方网站或者查询资料,在这个过程中你可能会接触到类似协变、逆变等概念,了解到infer
的更深层工作原理。不过这些不在本文的讨论范围之内,有兴趣的同学可以自行探索。
而我们的目标是为实战服务:掌握infer
的使用方法。所以让我们通过更多栗子和练习来达到这一目的吧。
还是Pop & Shift
让我们回到上面讲的Pop
。我们需要把数组里的元素分成两部分,一部分是前面的所有元素,另一部分是最后一个元素:这样我们就才用infer去占位到最后一个元素上:
T extends [infer A,infer B]
但是这样写是不正确的,因为第一个部分是前面所有元素,所以应该是个Array<T>
,我们这里的A
相当于只占位了第一个元素。
而占位Array<T>
的时候我们需要对它进行解构,否则就变成嵌套数组了:
T extends [...infer A, infer B]
OK,这样我们就成功的分离出了最后一个元素,接下来只用返回A
就能达到Pop
的目的:
T extends [...infer A, infer B] ? A : never
不过我们发现B
并没有被使用,TS会因此对你发出警告。对于这种不使用的infer参数,我们只需要用_
占位符就好了:
T extends [...infer A, infer _] ? A : never
最后,Pop
的完整答案:
export type Pop<T extends unknown[]> = T["length"] extends 0 | 1
? []
: T extends [...infer A, infer _]
? A
: never
看到这里你应该有点理解用法了,所以可以尝试写一下Shift
,相信你能写出来的。
Tuple to Union
我们上面提到,T[number]
的写法可以将集合转换为由其元素组成的union,这其实Tuple to Union这道题的解法之一:
export type TupleToUnion<T extends readonly unknown[]> = T[number]
如果我们改下需求,要求函数不对泛型参数进行限制,而是在函数体里判断,如果不是元组就返回never
,又该怎么写呢?
根据上面学到的知识,你应该可以想到答案:
type TupleToUnion2<T> = T extends readonly [... infer A] ? A[number] : never;
挑战题
Flatten
使用infer
来获取数组中的元素,想必已经难不倒你了:
type Flatten<T> = T extends Array<infer Item> ? Item : T;
那么现在来挑战一个难度高点的Flatten
吧,先上用例:
type Result = Flatten<[1, 2, [3, 4], [[[5]]]]> // [1, 2, 3, 4, 5]
如代码所示,这个Flatten
用于扁平化数组,不论数组的嵌套有多深。
相信你可以想到该题应同时使用递归和infer
了,那么就尝试一下吧!
AnyOf
实现AnyOf
,作用是:如果传入集合中有任意一个成员为falsy
值,则返回true,否则返回false:
type Sample1 = AnyOf<[1, "", false, [], {}]>; // expected to be true.
type Sample2 = AnyOf<[0, "", false, [], {}]>; // expected to be false.
这里的falsy
值为:
type Falsy = 0 | "" | null | undefined | [] | Record<any, never> | false
思路和上题差不多,赶快挑战一下吧!
写在最后
如果你仍感到困难,很正常,因为没有什么东西是看一篇文章就能完全掌握的。
所以还是推荐大家多去type challenges上练习,如果你觉得对某块薄弱,可以选则tag分类,专攻某一部分的题目。
如果你觉得某一个题目实在写不出来,可以先点击check out Solutions
看看别人的解法;同样的,如果你解出了一道很难的题,也建议你去看看别人的解法,因为很多题目都不止一种写法,而有时别人的写法会更简单,能给你启发。
类型体操要讲的内容其实已经不多了,很多小技巧需要大家在练习中自己掌握,一起加油吧!