typescript-challenges-easy

所有挑战来源type-challenges题库。

本节条件判断均使用Equal来验证,副验证Expect

代码如下:

1
2
3
4
5
6
7
type Equal<X, Y> = (<T>() => T extends X ? 1 : 2) extends <T>() => T extends Y
? 1
: 2
? true
: false;

type Expect<T extends true> = T;

在 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>

Challenge Readme

通过从 Type 中选取属性集 Keys (字符串文字或字符串文字的并集)来构造类型。

代码如下:

1
2
3
type MyPick<T, K extends string | symbol | number> = {
[Key in keyof T & K]: T[Key];
};

核心知识点:交叉类型 & 条件类型 extends 映射类型 in

关键点
交叉类型与类型约束(keyof T & K) 表示取类型 T 的所有属性名称与 K 类型的交集,即只包含 T 中存在的且在 K 中声明的属性名称。这样做是为了确保挑选的属性名称在类型 T 中是存在的,并且符合 K 的约束。
映射类型: 使用了映射类型 [Key in (keyof T & K)]: T[Key],其中 Key 表示每个属性名称,通过 in 关键字对 T 类型的属性名称进行遍历。这样做可以确保新类型中只包含原类型中存在的属性,并且属性的值的类型与原类型中对应属性的类型相同。

Readonly<T>

Challenge Readme

构造一个类型,并将 Type 的所有属性设置为 readonly ,这意味着构造类型的属性无法重新分配。

代码如下:

1
2
3
type MyReadonly<T> = {
readonly [key in keyof T]: T[key];
};

核心知识点:readonly keyof 映射类型 in

TupleToObject<T>

Challenge Readme

给定数组,将其转换为对象类型,并且键/值必须在提供的数组中。

官方没有这个 Utility Type,可以参考TupleToObject

代码如下:

1
2
3
type TupleToObject<T extends readonly (string | number | symbol)[]> = {
[P in T[number]]: P;
};

核心知识点:元组 索引类型查询 T[number] 映射类型 in

关键点
索引类型查询T[number] 表示元组类型 T 中的每个元素的联合类型。它使用了索引类型查询,返回元组 T 中每个元素的类型的联合。

First<T>

Challenge Readme

实现一个通用的First<T>,它接受一个数组T并返回其第一个元素的类型

代码如下:

1
2
3
4
5
6
7
8
//answer1
type First<T extends any[]> = T extends [] ? never : T[0];

//answer2
type First<T extends any[]> = T["length"] extends 0 ? never : T[0];

//answer3
type First<T extends any[]> = T extends [infer A, ...infer rest] ? A : never;

核心知识点:索引访问类型 T['length'] 条件类型 T extends A ? A : B 元组解构 [infer A, ...infer rest]

索引访问类型 T['length'] 与 索引类型查询 T[number] 有所不同,前者返回的是数组的长度,后者返回的是数组中每个元素的联合类型。

元组解构: 使用了元组解构 [infer A, ...infer rest],它尝试将数组类型 T 解构为第一个元素 A 和剩余元素的数组 rest。如果解构成功,则返回第一个元素类型 A,否则返回 never 类型。

三个答案都使用了 TypeScript 中的泛型和条件类型机制,以不同的方式实现了获取数组类型的第一个元素类型的功能。

Length<T>

Challenge Readme

对于给定的元组,您需要创建一个通用的Length,请选择元组的长度

代码如下:

1
2
3
4
//answer1
type Length<T extends readonly unknown[]> = T["length"];
//answer2
type Length<T extends Readonly<Array<unknown>>> = T["length"];

核心知识点:索引访问类型 T['length'] 类型约束 extends

注意点

  1. answer1 为简写形式,answer2 为完整形式,不可以串用,比如 readonly Array<unknown> 会导致错误。
  2. 之所以使用 readonly, 是因为在 Test Cases 中使用了 as const。 当使用 as const 时,它告诉 TypeScript 将值视为不可变的字面量类型,而不是可变的。这对于确保 TypeScript 能够准确推断出数组的内容类型非常重要,尤其是在使用 Length<T> 类型时,因为它依赖于数组确切的类型。

Exclude<T, U>

Challenge Readme

T 中排除可分配给 U 的类型

代码如下:

1
type MyExclude<T, U> = T extends U ? never : T;

核心知识点:索引访问类型 T['length']

注意点
关于条件类型 extends,在使用场景不同的情况下其输出结果也会有不同,比如下面例子:

1
2
3
4
5
type MyExclude<T, U> = T extends U ? never : T;
type A = "s" | "n" | "q";
type B = "s" | "n";
type C = A extends B ? never : A; // 's' | 'n' | 'q'
type D = MyExclude<A, B>; // 'q'

可以看到,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 Atype B 作为泛型参数传入(此时的 A 和 B 是分布式的,即会分解成单个元素),它在定义中使用了条件类型 T extends U ? never : T。这个条件类型会对 T 中的每个元素进行遍历和检查,根据条件排除满足条件的元素,因此得到的结果是排除了 U 中的元素后的 T 类型。

Awaited<T>

Challenge Readme

这种类型旨在对 async 函数中的 awaitPromise 上的 .then() 方法等操作进行建模 - 具体来说,他们递归地解开 Promise

代码如下:

1
2
3
4
5
6
type MyAwaited<T extends PromiseLike<any | PromiseLike<any>>> =
T extends PromiseLike<infer U>
? U extends PromiseLike<any>
? MyAwaited<U>
: U
: never;

核心知识点:条件类型 extends 泛型推断 infer 递归类型

关键点
泛型推断(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>

Challenge Readme

实现元类型 If<C,T,F>,它接受条件 C、真值 T 和假值 F。

代码如下:

1
type If<C extends boolean, T, F> = C extends true ? T : F;

核心知识点:条件类型 extends

Concat<T, U>

Challenge Readme

在类型系统中实现 JavaScript Array.concat 函数。类型接收两个参数。输出应是一个新数组,其中按 LTR 顺序包含输入内容

代码如下:

1
type Concat<T extends readonly any[], U extends readonly any[]> = [...T, ...U];

核心知识点:元组展开 ...

Includes<T, U>

Challenge Readme

在类型系统中实现 JavaScript Array.includes 函数。类型包含两个参数。输出应为布尔值 true 或 false。

代码如下:

1
2
3
4
5
6
7
8
type Includes<T extends readonly unknown[], U> = T extends [
infer First,
...infer Rest
]
? Equal<First, U> extends true
? true
: Includes<Rest, U>
: false;

核心知识点:条件类型 extends 泛型推断 infer 递归类型

这题并不 easy,与 exclude 有些许不同。(详细跳转 Challenge Readme)

类型推断过程:

  1. T extends [infer First, ...infer Rest]:将 T 分解为 [First, ...Rest],并判断 T 是否为空数组。
  2. Equal<First, U> extends true ? true : Includes<Rest, U>:如果 T 不为空数组,则判断 First 是否等于 U,如果相等,则返回 true,否则递归调用 Includes
  3. 如果 T 为空数组,则返回 false。

提问:为什么 includes 函数需要使用递归?而 exclude 函数不需要?
因为 includes 函数需要判断数组中是否存在某个元素,而 exclude 函数只需要判断数组中是否不存在某个元素即可。

Push<T[ ], T>

Challenge Readme

实现 array.push 的通用版本

1
type Push<T extends unknown[], U> = [...T, U];

核心识点:元组展开 ...

Unshift<T[ ], T>

Challenge Readme

实现 array.unshift 的类型版本

1
type Unshift<T extends unknown[], U> = [U, ...T];

核心知识点:元组展开 ...

Parameters<T>

Challenge Readme

根据函数类型 Type 的参数中使用的类型构造元组类型,不使用内置 Parameters

1
2
3
4
type MyParameters<T extends (...args: any[]) => unknown> = 
T extends (...unknown: infer S) => unknown
? S
: unknown;

核心知识点:元组展开 ... 泛型推断 infer 条件类型 extends

关键点
泛型推断(Generic Inference):通过 infer 关键字,推断出函数类型中包含的参数类型。在这里,我们使用 infer S 来代表参数类型,并在条件类型中进一步操作。

$The\,End$

强烈推荐大家动手练习type-challenges,通过思考和实践,相信你们能够轻松掌握其中的技巧。
以上内容为 type-challenges 13 部分,后续继续更新97部分,随着内容的增加,我们将把它们分为更多期,一期将分为 15 个挑战。
感谢你的阅读,期望能够给你带来一些启发与收获。