typescript-challenges-easy
所有挑战来源type-challenges题库。
本节条件判断均使用Equal
来验证,副验证Expect
代码如下:
1 | type Equal<X, Y> = (<T>() => T extends X ? 1 : 2) extends <T>() => T extends Y |
在 ts 中,泛型函数的条件判断可以用类型推断来比较两个类型。Equal
在提供的类型验证中,使用了两个泛型函数,它们都是立即执行函数。
在这些函数中,通过使用条件类型 <T>() => T extends X ? 1 : 2 和 <T>() => T extends Y ? 1 : 2
对类型 X 和 Y 进行检查。
这里的 1 和 2 实际上是两个不同的类型分支标识符。当条件 <T>() => T extends X ? 1 : 2
成立时,返回 1,表示类型 X 和当前泛型 T 匹配,比较类型 Y 的逻辑与之类似,然后利用 extends
条件类型关键字进行类型推断,如果类型 X 和 Y 匹配,则返回 true,否则返回 false。
知道了这两个条件判断,就可以通过类型推断来验证类型是否匹配,接下来进入正题。
Pick<T, K>
通过从 Type 中选取属性集 Keys (字符串文字或字符串文字的并集)来构造类型。
代码如下:
1 | type MyPick<T, K extends string | symbol | number> = { |
核心知识点:
关键点:
交叉类型与类型约束: (keyof T & K)
表示取类型 T 的所有属性名称与 K 类型的交集,即只包含 T 中存在的且在 K 中声明的属性名称。这样做是为了确保挑选的属性名称在类型 T 中是存在的,并且符合 K 的约束。
映射类型: 使用了映射类型 [Key in (keyof T & K)]: T[Key]
,其中 Key 表示每个属性名称,通过 in 关键字对 T 类型的属性名称进行遍历。这样做可以确保新类型中只包含原类型中存在的属性,并且属性的值的类型与原类型中对应属性的类型相同。
Readonly<T>
构造一个类型,并将 Type
的所有属性设置为 readonly
,这意味着构造类型的属性无法重新分配。
代码如下:
1 | type MyReadonly<T> = { |
核心知识点:
TupleToObject<T>
给定数组,将其转换为对象类型,并且键/值必须在提供的数组中。
官方没有这个 Utility Type,可以参考TupleToObject
代码如下:
1 | type TupleToObject<T extends readonly (string | number | symbol)[]> = { |
核心知识点:
关键点:
索引类型查询: T[number]
表示元组类型 T 中的每个元素的联合类型。它使用了索引类型查询,返回元组 T 中每个元素的类型的联合。
First<T>
实现一个通用的First<T>
,它接受一个数组T
并返回其第一个元素的类型
代码如下:
1 | //answer1 |
核心知识点:
索引访问类型
T['length']
与 索引类型查询T[number]
有所不同,前者返回的是数组的长度,后者返回的是数组中每个元素的联合类型。
元组解构: 使用了元组解构 [infer A, ...infer rest]
,它尝试将数组类型 T 解构为第一个元素 A 和剩余元素的数组 rest。如果解构成功,则返回第一个元素类型 A,否则返回 never
类型。
三个答案都使用了 TypeScript 中的泛型和条件类型机制,以不同的方式实现了获取数组类型的第一个元素类型的功能。
Length<T>
对于给定的元组,您需要创建一个通用的Length
,请选择元组的长度
代码如下:
1 | //answer1 |
核心知识点:
注意点:
- answer1 为简写形式,answer2 为完整形式,不可以串用,比如
readonly Array<unknown>
会导致错误。 - 之所以使用
readonly
, 是因为在 Test Cases 中使用了as const
。 当使用as const
时,它告诉 TypeScript 将值视为不可变的字面量类型,而不是可变的。这对于确保 TypeScript 能够准确推断出数组的内容类型非常重要,尤其是在使用Length<T>
类型时,因为它依赖于数组确切的类型。
Exclude<T, U>
从 T
中排除可分配给 U
的类型
代码如下:
1 | type MyExclude<T, U> = T extends U ? never : T; |
核心知识点:
注意点:
关于条件类型 extends
,在使用场景不同的情况下其输出结果也会有不同,比如下面例子:
1 | type MyExclude<T, U> = T extends U ? never : T; |
可以看到,type C
的输出结果是 's' | 'n' | 'q'
,而 type D 的输出结果是 'q'
。
在 type C
中,条件类型 A extends B ? never : A
在判断 A 是否是 B 的子类型时,根据条件直接返回了整个 type A
(此时的 A 不是分布式的,即不会分解成单个元素),而不会进一步检查 A 中的每个元素。
而在 type D
中,使用的是自定义的 MyExclude 类型,type A
和 type B
作为泛型参数传入(此时的 A 和 B 是分布式的,即会分解成单个元素),它在定义中使用了条件类型 T extends U ? never : T
。这个条件类型会对 T 中的每个元素进行遍历和检查,根据条件排除满足条件的元素,因此得到的结果是排除了 U 中的元素后的 T 类型。
Awaited<T>
这种类型旨在对 async
函数中的 await
或 Promise
上的 .then()
方法等操作进行建模 - 具体来说,他们递归地解开 Promise
。
代码如下:
1 | type MyAwaited<T extends PromiseLike<any | PromiseLike<any>>> = |
核心知识点:
关键点:
泛型推断(Generic Inference):通过 infer
关键字,我们可以让 TypeScript 推断出 PromiseLike
中包含的实际类型。这使得我们在处理嵌套的 PromiseLike
值时能够使用 infer
关键字来暂时代表这个类型,并在条件类型中进一步判断和操作。
递归类型(Recursive Types):在 MyAwaited
类型定义中,我们对 U 进行了递归调用,以处理可能嵌套的 PromiseLike
值。这使得我们能够在 U 是 PromiseLike
类型时继续解析,直到达到最内部的非 PromiseLike
值。
在不考虑错误:Unused ‘@ts-expect-error’ directive.(2578) 的情况下,使用
type MyAwaited<T> = T extends PromiseLike<infer U> ? MyAwaited<U> : T
即可通过测试。但是为了保证 case test 的覆盖率,当 T 不是PromiseLike
类型时,应该返回never
类型更加合理,为此在推断 U 之前添加一层判断,如果 T 不是PromiseLike
类型,则直接返回never
类型。
If<C, T, F>
实现元类型 If<C,T,F>
,它接受条件 C、真值 T 和假值 F。
代码如下:
1 | type If<C extends boolean, T, F> = C extends true ? T : F; |
核心知识点:
Concat<T, U>
在类型系统中实现 JavaScript Array.concat
函数。类型接收两个参数。输出应是一个新数组,其中按 LTR
顺序包含输入内容
代码如下:
1 | type Concat<T extends readonly any[], U extends readonly any[]> = [...T, ...U]; |
核心知识点:
Includes<T, U>
在类型系统中实现 JavaScript Array.includes
函数。类型包含两个参数。输出应为布尔值 true 或 false。
代码如下:
1 | type Includes<T extends readonly unknown[], U> = T extends [ |
核心知识点:
这题并不 easy,与 exclude 有些许不同。(详细跳转 Challenge Readme)
类型推断过程:
T extends [infer First, ...infer Rest]
:将 T 分解为[First, ...Rest]
,并判断 T 是否为空数组。Equal<First, U> extends true ? true : Includes<Rest, U>
:如果 T 不为空数组,则判断 First 是否等于 U,如果相等,则返回 true,否则递归调用 Includes。 - 如果 T 为空数组,则返回 false。
提问:为什么 includes 函数需要使用递归?而 exclude 函数不需要?
因为 includes 函数需要判断数组中是否存在某个元素,而 exclude 函数只需要判断数组中是否不存在某个元素即可。
Push<T[ ], T>
实现 array.push
的通用版本
1 | type Push<T extends unknown[], U> = [...T, U]; |
核心识点:
Unshift<T[ ], T>
实现 array.unshift
的类型版本
1 | type Unshift<T extends unknown[], U> = [U, ...T]; |
核心知识点:
Parameters<T>
根据函数类型 Type 的参数中使用的类型构造元组类型,不使用内置 Parameters
。
1 | type MyParameters<T extends (...args: any[]) => unknown> = |
核心知识点:
关键点:
泛型推断(Generic Inference):通过 infer 关键字,推断出函数类型中包含的参数类型。在这里,我们使用 infer S
来代表参数类型,并在条件类型中进一步操作。
$The\,End$
强烈推荐大家动手练习type-challenges,通过思考和实践,相信你们能够轻松掌握其中的技巧。
以上内容为 type-challenges 部分,后续继续更新部分,随着内容的增加,我们将把它们分为更多期,一期将分为 15 个挑战。
感谢你的阅读,期望能够给你带来一些启发与收获。