持续创作,加速成长!这是我参与「掘金日新计划 · 10 月更文挑战」的第 2 天,点击查看活动详情
前言
在上一篇文章中我们了解到了可以利用递归来解决 TS 中遇到的一些问题,在本文中,我们将继续实现 type-challenges 中的 Tuple to Object 以及拓展题 Tuple to Union,Tuple to Nested Ocject。
在本文中,我们将了解到 tuple 类型的一些特殊操作,以及如何使用 infer 来进行推断,与递归操作结合,最终解决问题。
Tuple to Object
在本题中,我们将实现将一个数组转化为一个对象,这个对象的 key 与 value 必须是相同的,且 key\value 在该数组中
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'}
测试用例
import type { Equal, Expect } from '@type-challenges/utils'
const tuple = ['tesla', 'model 3', 'model X', 'model Y'] as const
const tupleNumber = [1, 2, 3, 4] as const
const tupleMix = [1, '2', 3, '4'] as const
type cases = [
Expect<Equal<TupleToObject<typeof tuple>, { tesla: 'tesla'; 'model 3': 'model 3'; 'model X': 'model X'; 'model Y': 'model Y' }>>,
Expect<Equal<TupleToObject<typeof tupleNumber>, { 1: 1; 2: 2; 3: 3; 4: 4 }>>,
Expect<Equal<TupleToObject<typeof tupleMix>, { 1: 1; '2': '2'; 3: 3; '4': '4' }>>,
]
// @ts-expect-error
type error = TupleToObject<[[1, 2], {}]>
根据题意与测试用例,我们可以提取出以下几点
- 需要传入一个 tuple 类型,且 tuple 中的类型不能是数组与对象
- 需要对传入的 T 进行遍历
- 返回一个对象,对象的 key/value 必须是相同的
相信通过前两篇文章的阅读,我们已经对这些条件相当熟悉了,所以我们就不在先通过 JS 来实现这个题目,直接使用 TS 来进行编写。
对传入的数据进行判断
在编写相关代码之前,我们先对测试用例中出现的知识点进行分析。在测试用例中,我们声明了一些数组并用 as const 将它们转化为字面量类型(literal type),在这之后再使用 typeof 将这些数组转化为 tuple。
当我们在使用 const 对变量进行声明时,如 const world = "world" 时,TS 会自动将 world 的类型转化为字面量类型 world。同理,我们使用 as const 对 tupleNumber 数组进行类型断言,那么该数组的类型会从 number[] 转化为 tuple,它的值为 [1,2,3,4]。
再回到 typeof 操作符,我们要求在传入参数时,传入一个类型,而不是一个数组,在这时候 typeof 的作用就体现出来了。形象地来说,typeof 就像是连接 JS 世界与 TS 世界的一个通道。当我们想要在 TS 世界使用 JS 世界内的数据时,我们可以使用 typeof 来将 JS 世界内的数据映射到 TS 中。
通过以上解释,我们就不难理解为什么测试用例中要对数据进行这两步处理,接下来,我们来对我们的题目进行编写
type TupleToObject<T extends readonly PropertyKey[]> = {}
在这段代码中,我们利用 Propertykey 以及 readonly 来对 T 传入的类型进行判断。因为传入的 T 是以字面量类型组成的 tuple,我们不可以对其进行修改,所以使用 readonly 对其进行限制。
我们之后再使用 PropertyKey 来对 tuple 中的类型进行限制。Propertykey 的默认值是一个 union,这个 union 的默认值为 string | number | symbol。 这样我们就完成了第一个点
对传入的 T 进行遍历,对象的 key/value 必须是相同的
因为后两个点其实是两两关联的,所以合作一点说明
type TupleToObject<T extends readonly PropertyKey[]> = {
[P in T[number]]: P
}
在使用 in 对 T 进行遍历的时候,P 可以看作 T 中的每一项值,所以我们只要使用 P 来作为 value 就可以完成第三点。
值得注意的是,in 操作符右边需要传入一个 union。对于对象,我们可以使用 keyof 来对对象进行操作,他会返回一个以对象的属性组成的 union。而我们这次传入的是一个 tuple,所以我们这次使用 T[number] 来将数组内的数据取出并转化为 union。
至此这道题目已经完整解出。
Tuple to Union
在这道题中,我们将实现一个将 tuple 转换为 union 的泛型 TupleToUnion<T>
type Arr = ['1', '2', '3']
type Test = TupleToUnion<Arr> // expected to be '1' | '2' | '3'
测试用例
import type { Equal, Expect } from '@type-challenges/utils'
type cases = [
Expect<Equal<TupleToUnion<[123, '456', true]>, 123 | '456' | true>>,
Expect<Equal<TupleToUnion<[123]>, 123>>,
]
在这道题中,我将提供两种解法。
- 解法一:利用 tuple 本身的特性来解决
- 解法二:利用递归与 infer 来进行解答
相信如果阅读了上一道题的解答过程,我们可以很容易的想到可以利用 T[number] 来将一个 tuple 转化为 union。
type TupleToUnion<T extends any[]> = T[number]
下面我们来着重讲解第二种解答,利用递归与 infer 来进行解答。先给出答案,之后再一步步分析解题过程
type TupleToUnion<T> = T extends [infer First, ...infer Rest] ? First | TupleToUnion<Rest>: never
在开始分析时,我们不妨先搞明白 infer 它到底是干什么的。在官方文档中,他用一句话
Within the extends clause of a conditional type, it is now possible to have infer declarations that introduce a type variable to be inferred.
翻译过来就是,在条件类型的子句中,现在可以使用 infer 声明来引入要推断的类型变量。
也就是说,我们可以在 extends 后面使用 infer 来对类型变量进行声明引入。是不是听起来十分的拗口,我们不妨使用数学中设置未知变量的思维来理解他。在数学中,当我们不知道某个变量的具体值时,我们可以设置一个未知变量 X 来指代他,方便我们进行下一步操作。所以 infer 的作用也是正是如此,我们可以设置一个未知的类型变量来进行下一步操作。
在本题中我们使用 [infer First, ...infer Rest] 来对 T 中的类型进行假设,其中利用了 rest parameter 来对数组的剩余参数进行合并。如果当前的 T 满足这个推断则进行 First | TupleToUnion<Rest> 这个分支,再将 Rest 参数传入进行递归调用,最终得到结果。
至此,此题已经解决。如果对 infer 还有疑惑,我会将 TS 官方文档对 infer 的解释的链接放在最后,看完官方文档及示例后,相信你会完全理解 infer。
Tuple to Nested Object
在本题中,传入一个只包含字符串类型的元组 T,以及一个类型 U,以递归的方式构建一个对象
type a = TupleToNestedObject<['a'], string> // {a: string}
type b = TupleToNestedObject<['a', 'b'], number> // {a: {b: number}}
type c = TupleToNestedObject<[], boolean> // boolean. if the tuple is empty, just return the U type
测试用例
import type { Equal, Expect } from '@type-challenges/utils'
type cases = [
Expect<Equal<TupleToNestedObject<['a'], string>, { a: string }>>,
Expect<Equal<TupleToNestedObject<['a', 'b'], number>, { a: { b: number } }>>,
Expect<Equal<TupleToNestedObject<['a', 'b', 'c'], boolean>, { a: { b: { c: boolean } } }>>,
Expect<Equal<TupleToNestedObject<[], boolean>, boolean>>,
]
乍一看我们好像并没有什么思路,那么我们不妨先使用 JS 的思维进行解答
function tuple(arr,type) {
const [first, ...rest] = arr
if (arr.length === 0) {
return type
}
return {
[first]: tuple(rest,type)
}
}
当我们使用 JS 进行递归解答,我们可以很容易的得出结果。从中我们再对其进行要点提取
- 对传入的 T 进行解构
- 返回一个对象,该对象的 key 为解构出的第一个值,value 为递归调用的函数,同时将剩余参数 rest 传入
- 当传入的 T 的不满足推断的格式时,结束递归,返回 U
下面我们来一步步实现整个题目
对类型进行解构,当 T 不满足时返回 U
经过前面两道题的练习,很容易的就可以想到用 [infer First, ...infer Rest] 来对 T 进行解构,或者说是推断 T 中的类型,而 infer 通常是与 extends 结合使用来进行判断
type TupleToNestedObject<T, U> = T extends [infer F, ...infer R] ? {}: U
值得注意的是,在对 T 进行 extends 操作时,我们进行的是整个判断,所以当递归的 T 中的值不满足时,也就是只有一个值,无法解构出 rest 参数时,进入第二个分支,返回 U
返回一个对象进行递归操作
在上一步骤中,我们已经实现了返回一个对象。下面我们根据已经实现的 JS 代码可以容易的写出剩余部分
type TupleToNestedObject<T, U> = T extends [infer F extends PropertyKey, ...infer R] ? {
[P in F] : TupleToNestedObject<R, U>
} : U
值得注意的是,我们在对象中要使一个 union 变为 key 时,需要利用 in 操作符。而在对象中,key 必须是 Properkey 类型,所以需要添加一个约束来限制 F 的类型。
至此,所有题型已完整解出。
总结
- infer
- Rest Parameter
- typeof