概览
本文从type-challenges选取16道题目,进行详细分析、解题。
从另外一个角度,来审视自己所学TypeScript是否牢固,并进行查漏补缺。
希望可以给读者一个启发,用好这门强类型语言。
实现 Pick
实现 TS 内置的 Pick<T, K>
,但不可以使用它。
从类型 T
中选择出属性 K
,构造成一个新的类型。
例如:
1 | interface Todo { |
题目来源:https://tsch.js.org/4/zh-CN
解答
1 | type MyPick<T, K extends keyof T = keyof T> = { |
思路
keyof
取出key
MyPick<Todo, 'title' | 'completed'>
尖括号的右侧是'title' | 'completed'
,它们是Todo
类型的两个key。
所以,需要用keyof T
取出。keyof T
等于 'title' | 'completed' | 'description'
。
extends
对泛型进行约束
'title' | 'completed'
是 'title' | 'completed' | 'description'
的子集(keyof T
)。
所以 'title' | 'completed'
等于 K extends keyof T
。
in
遍历枚举类型
[P in K]
的意思是将K
中的key
遍历出来,赋予P
。T[P]
的意思是将T
中对应的key
=== P
的值取出来。
所以,[P in K]: T[P]
的含义就很明白了。
为什么要加 = keyof T
?
如果以MyPick<T>
中形式调用时,T
如果没有默认值,会报错。
所以,需要加上 = keyof T
。
Readonly
不要使用内置的Readonly<T>
,自己实现一个。
该 Readonly
会接收一个 _泛型参数_,并返回一个完全一样的类型,只是所有属性都会被 readonly
所修饰。
也就是不可以再对该对象的属性赋值。
例如:
1 | interface Todo { |
题目来源:https://tsch.js.org/7/zh-CN
解答
1 | type MyReadonly<T> = { |
使用readonly
关键字声明属性是只读属性。
使用keyof T
取出泛型T
中的所有key,再用in
遍历。
元组转换为对象
传入一个元组类型,将这个元组类型转换为对象类型,这个对象类型的键/值都是从元组中遍历出来。
例如:
1 | const tuple = ['tesla', 'model 3', 'model X', 'model Y'] as const |
题目来源:https://tsch.js.org/11/zh-CN
解答
1 | type TupleToObject<T extends readonly any[]> = { |
思路
T[number]
是什么?
先看看TypeScript的一个例子:
1 | type Flatten<T> = T extends any[] ? T[number] : T; |
泛型T
是数组,数组以number
为索引,所以T[number]
对应数组中的每个值。
[P in T[number]]
使用in
遍历T[number]
,也即遍历泛型数组T
中的每个值。
第一个元素First<T>
实现一个通用First<T>
,它接受一个数组T
并返回它的第一个元素的类型。
例如:
1 | type arr1 = ['a', 'b', 'c'] |
题目来源:https://tsch.js.org/14/zh-CN
解答
解法一
1 | type First<T extends any[]> = T['length'] extends 0 ? never : T[0]; |
判断数组长度,如果长度不是0,则代表0位有值,可以取0位。
解法二
1 | type First<T extends any[]> = T[0] extends T[number] ? T[0] : never; |
通过T[number]
代表数组单个项的值,推断T[0] extends T[number]
。
如果属于,则代表长度大于0,有值。
解法三
1 | type First<T extends any[]> = T extends [infer F] ? F : never |
通过infer
指代数组第一个项,如果T extends [infer F]
推断是true
,则得到第一个项type为F
。
解法四
1 | type First<T> = T extends [infer P, ...infer Rest] ? P : never |
使用扩展运算符,配合infer
,可以得到第一个项的type。
获取元组长度
创建一个通用的Length
,接受一个readonly
的数组,返回这个数组的长度。
例如:
1 | type tesla = ['tesla', 'model 3', 'model X', 'model Y'] |
题目来源:https://tsch.js.org/18/zh-CN
解答
1 | type Length<T extends readonly any[]> = T['length']; |
数组的长度,用T['length']
可以推导出来。
这里加readonly
的原因是,如果数组是用const
声明,则必须是readonly
。
实现 Exclude
实现内置的Exclude <T, U>类型,但不能直接使用它本身。
从联合类型T中排除U的类型成员,来构造一个新的类型。
题目来源:https://tsch.js.org/43/zh-CN
解答
1 | type MyExclude<T, U> = T extends U ? never : T; |
使用extends
条件推断,如果为false
,则是独立存在于T的集合。
Awaited
假如我们有一个 Promise 对象,这个 Promise 对象会返回一个类型。在 TS 中,我们用 Promise<T>
中的 T 来描述这个 Promise 返回的类型。请你实现一个类型,可以获取这个类型。
比如:Promise<ExampleType>
,请你返回 ExampleType 类型。
题目来源:https://tsch.js.org/189/zh-CN
解答
1 | type MyAwaited<T extends Promise<any>> = T extends Promise<infer U> ? U extends Promise<any> |
先用条件推断,泛型T
是否是Promise返回,并用infer U
指代返回值。U
有两种情况:
- 普通返回值类型
- Promise类型
如果U
是Promise
类型,则需要递归检查。对应的代码是:
1 | U extends Promise<any> ? MyAwaited<U> : U |
如果是普通返回值类型,则直接返回U
。
为什么要加extends Promise<any>
?
MyAwaited<T extends Promise<any>>
的含义,是为了避免用户传入非Promise function。
如果用户违反规则,TypeScript会按报错处理。
IF
实现一个 IF
类型,它接收一个条件类型 C
,一个判断为真时的返回类型 T
,以及一个判断为假时的返回类型 F
。 C
只能是 true
或者 false
, T
和 F
可以是任意类型。
举例:
1 | type A = If<true, 'a', 'b'> // expected to be 'a' |
题目来源:https://tsch.js.org/268/zh-CN
解答
1 | type If<C extends boolean, T, F> = C extends false ? F : T; |
题意要求,C
必须是boolean类型,所以对非boolean类型的传值,应该按报错处理。
使用C extends boolean
进行限制。
使用extends
进行条件推断,即可根据判断决定返回的值。
Concat
在类型系统里实现 JavaScript 内置的 Array.concat
方法,这个类型接受两个参数,返回的新数组类型应该按照输入参数从左到右的顺序合并为一个新的数组。
举例,
1 | type Result = Concat<[1], [2]> // expected to be [1, 2] |
题目来源:https://tsch.js.org/533/zh-CN
解答
1 | type Concat<T extends any[], U extends any[] > = [...T, ...U]; |
先用extends any[]
限制泛型T
和U
是数组类型。
接着,就可以使用扩展运算符进行扩展数组。
Includes
在类型系统里实现 JavaScript 的 Array.includes
方法,这个类型接受两个参数,返回的类型要么是 true
要么是 false
。
举例来说,
1 | type isPillarMen = Includes<['Kars', 'Esidisi', 'Wamuu', 'Santana'], 'Dio'> // expected to be `false` |
题目来源:https://tsch.js.org/898/zh-CN
解答
1 | type Equal<X, Y> = (<T>() => T extends X ? 1 : 2) extends (<T>() => T extends Y ? 1 : 2) ? true : false; |
解题的重点有两个:
- 判断两个类型相等,需要实现
Equal
- 逐个拆解类型数组T,将单个项取出来比较
Equal
<T>() => T extends X ? 1 : 2
和 (<T>() => T extends Y ? 1 : 2)
: 取出比较参数的类型。
如果X
,Y
不相等,则第1个表达式取到的数字,和第2个表达式取到的数字不一样。
原因是T只能是一种类型。(1只脚不能同时踏入两条河流的哲学问题:))
逐个取出数组中的值
T extends [infer K, ...infer R]
用于提取数组中的值,并使用Equal<U, K>
进行比较。
如果不相等,则用Includes<R, U>
递归处理。
Push
在类型系统里实现通用的 Array.push
。
举例如下,
1 | type Result = Push<[1, 2], '3'> // [1, 2, '3'] |
题目来源:https://tsch.js.org/3057/zh-CN
解答
1 | type Push<T extends any[], U> = [...T, U]; |
使用extends any []
限制泛型T
为数组类型,
然后,使用扩展运算符展开T
,进行合并。
Pop
实现一个通用Pop<T>
,它接受一个数组T
并返回一个没有最后一个元素的数组。
例如
1 | type arr1 = ['a', 'b', 'c', 'd'] |
题目来源:https://tsch.js.org/16/zh-CN
解答
1 | type Pop<T extends any[]> = T extends [...infer K, infer U] ? K : never; |
使用infer
进行指代,配合解构,即可解开此题。
Unshift
实现类型版本的 Array.unshift
。
举例,
1 | type Result = Unshift<[1, 2], 0> // [0, 1, 2,] |
题目来源:https://tsch.js.org/3060/zh-CN
解答
1 | type Unshift<T extends any[], U> = [U, ...T]; |
使用extends any []
限制泛型T
为数组类型,
然后,使用扩展运算符展开T
,进行合并。
实现内置的 Parameters<T>
类型
实现内置的 Parameters<T>
类型,而不是直接使用它,可参考TypeScript官方文档。
题目来源:https://tsch.js.org/3312/zh-CN
解答
1 | type MyParameters<T extends (...args: any[]) => any> = T extends (...args: infer U) => any ? U : never; |
使用infer U
指代参数列表,就可以正确推导出类型。
获取函数返回类型
不使用 ReturnType
实现 TypeScript 的 ReturnType<T>
泛型。
例如:
1 | const fn = (v: boolean) => { |
题目来源:https://tsch.js.org/2/zh-CN
解答
1 | type MyReturnType<T> = T extends (...args: any[]) => infer U ? U : never; |
使用infer U
指代返回值类型即可。
计算字符串的长度
计算字符串的长度,类似于 String#length
。
题目来源:https://tsch.js.org/298/zh-CN
解答
1 | type LengthOfString<S extends string, T extends any[] = []> = S extends `${infer L}${infer R}` ? |
解题的关键点有两个:
- 增加参数
T
,默认是空数组,用于存放读取的字符,方便使用数组的length
属性,得到长度 - 使用递归,逐个拆解字符串
1 | S extends `${infer L}${infer R}` |
对字符串进行拆解
如果当前的字符串已拆解完,则读取存放数组T
的长度。
如果没有拆解完,则递归调用LengthOfString<R, [...T, L]>
,并将取出的字符,放入T
中。