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 个挑战。
感谢你的阅读,期望能够给你带来一些启发与收获。