TS体操类型学习记录
2026一百道题计划(17 / 300)
Easy
1. Easy - 4 - Pick
从类型 T 中选出符合 K 的属性,构造一个新的类型
type MyPick<T, K extends keyof T> = {
[key in K]: T[key]
}
keyof是 TypeScript 中的一个关键字,用于获取一个类型的所有属性名组成的联合类型。extends可以用于约束泛型类型参数、定义类型继承关系和条件类型的判断。- 在映射类型中,可以通过
in关键字遍历联合类型,可以用于遍历一个类型的属性名(需要通过keyof获取属性名组成的联合类型),并对每个属性进行相应的操作。
2. Easy - 7 - Readonly
泛型 Readonly<T> 会接收一个泛型参数,并返回一个完全一样的类型,只是所有属性都会是只读 (readonly) 的。
type MyReadonly<T> = {
readonly [key in keyof T]: T[key];
}
- 在 TypeScript 中,可以使用 readonly 修饰符来指定一个只读属性。
3. Easy - 11 - Tuple to Object
将一个元组类型转换为对象类型,这个对象类型的键/值和元组中的元素对应。
type TupleToObject<T extends readonly (string | number | symbol)[]> = {
[key in T[number]]: key
}
- 表示多种类型的数据,先通过
(string | number | symbol)表示一个联合,然后(string | number | symbol)[]表示联合类型的数组 - 元组类型是 TypeScript 中的一种特殊数据类型,它允许我们定义一个固定长度和固定类型顺序的数组。
- 通过
T[number]可以获取元组T所有成员组成的联合类型。 - 加readonly 的作用,因为测试用例定义的变量都是
const,const的作如下:
const tupleNumber = [1, 2, '33'] // (string | number)[]
const tupleNumber = [1, 2, '33'] as const // readonly [1, 2, "33"]
4. Easy - 14 - First of Array
实现一个 First<T> 泛型,它接受一个数组 T 并返回它的第一个元素的类型。
// 解法一:判断数组长度是否为0
type First<T extends any[]> = T['length'] extends 0 ? never : T[0]
// 解法二:判断是否为空数组
type First<T extends any[]> = T extends [] ? never : T[0]
// 解法三:获取T所有元素类型组成的联合类型,如果为never证明为空数组
type First<T extends any[]> = T[number] extends never ? never : T[0];
// 解法四:我们可以通过 keyof T 来获取类型 T 所有键的类型组合,那么如果 '0' 是 T 的键就表示 T 是一个非空数组
type First<T extends any[]> = '0' extends keyof T ? T[0] : never;
// 解法五:infer进行模式匹配
type First<T extends any[]> = T extends [infer A, ...infer _rest] ? A : never
- 在 TS 中
A extends B ? C : D的意思是“如果A可以赋值给B,那么类型就是C,否则就是D”。 - T[0] 获取数组
T下标为 0 的元素类型 - 在 TS 中,我们通过 extends 和 infer 可以实现一个类似于模式匹配的效果,
- 其中 ...infer _rest 是我们对元组类型使用的扩展运算,在这里用于表示剩余元素。
5. Easy - 18 - Length of Tuple
创建一个Length泛型,这个泛型接受一个只读的元组,返回这个元组的长度。
type Length<T extends readonly unknown[]> = T['length']
T['length']是索引访问类型,用于获取对象/数组类型的属性类型- 对于元组(tuple),
length是字面量数字类型(如 3) - 对于普通数组,
length是 number 类型
6. Easy - 43 - Exclude ※
从联合类型 T 中排除 U 中的类型,来构造一个新的类型。
type MyExclude<T, U> = T extends U ? never : T;
- 当
T extends U ? ...中的T为联合类型时,会把联合类型中的每一个类型单独进行判断,然后再把结果组合成一个联合类型返回。 - 如果你想避免这种行为,那么使用 [] 包裹你的类型参数即可,注意在 extends 关键字的两侧都需要:
[T] extends [U] ? ...
7. Easy - 189 - Awaited
假如我们有一个 Promise 对象,这个 Promise 对象会返回一个类型。在 TS 中,我们用 Promise 中的 T 来描述这个 Promise 返回的类型。请你实现一个类型,可以获取这个类型。
interface Thenable<T> {
then: (onfulfilled: (arg: T) => any) => any
}
type MyAwaited<T extends Promise<any> | Thenable<any>> = T extends Promise<infer A> | Thenable<infer A>
? A extends Promise<infer _X> | Thenable<infer _X> ? MyAwaited<A> : A
: never
8. Easy - 268 - IF
实现一个 IF 类型,它接收一个条件类型 C ,一个判断为真时的返回类型 T ,以及一个判断为假时的返回类型 F。 C 只能是 true 或者 false, T 和 F 可以是任意类型。
type If<C extends boolean, T, F> = C extends true ? T : F;
通过 C extends true 来判断 C 是否为 true。
9. Easy - 533 - Concat
在类型系统里实现 JavaScript 内置的 Array.concat 方法,这个类型接受两个参数,返回的新数组类型应该按照输入参数从左到右的顺序合并为一个新的数组。
type Concat<T extends readonly unknown[], U extends readonly unknown[]> = [...T, ...U]
- 在 TS 中,扩展语法可以对元组类型使用,用法类似于在 JS 中对值的使用
10. Easy - 898 - Includes ※
在类型系统里实现 JavaScript 的 Array.includes 方法,这个类型接受两个参数,返回的类型要么是 true 要么是 false。
一开始我想的是 type Includes<T extends readonly any[], U> = U extends T[number] ? true : false
但是 extends 无法保证全等, Expect<Equal<Includes<[{}], { a: 'A' }>, false>> 比如这个 case 会失败。
正确答案为
type Equal<X, Y> = (<T>() => T extends X ? 1 : 2) extends <T>() => T extends Y ? 1 : 2
? true
: false;
type Includes<T extends readonly unknown[], U> =
T extends [infer F, ...infer Rest]
? Equal<F, U> extends true ? true : Includes<Rest, U>
: false;
- 其中
[infer F, ...infer Rest]来提取出元组中的第一个类型,然后通过递归的方式,依次判断每一个类型元素是否和U相等。 - Equal 实现原理很复杂,暂时搞不懂,记住就行
11. Easy - 3057 - Push
type Push<T extends unknown[], U> = [...T, U]
做过上面的 Concat 这道题就很简单了,注意需要通过 T extends unknown[] 来限定 T 的类型,否则无法使用 ...T。
12. Easy - 3060 - Unshift
type Unshift<T extends unknown[], U> = [U, ...T]
13. Easy - 3312 - Parameters
实现内置的 Parameters 类型
这种题目一看就是要使用 infer 让 T extends xxx,其中 xxx 里面通过 infer 标记出入参的类型名。不过这里一开始写错了写成了
type MyParameters<T extends (...args: any[]) => any> =
T extends (infer A => any) ? A : never
错误的原因:把类型当变量来用了 在做类型体操的时候已经出了很多次错误了。谨记谨记。
正确解法:
type MyParameters<T extends (...args: any[]) => any> =
T extends (...args: infer A) => any ? A :never
- 使用
infer表示待推断的类型变量,由于...args本身已经是元组类型,因此infer P最终推导出的,也是元组类型。
Medium
14. Medium - 2 - Get Return Type 获取函数返回类型
和获取参数类型的逻辑是一模一样的。
type MyReturnType<T> = T extends (...args: any) => infer R ? R : never
15. Medium - 3 - 实现 Omit *
解法一,去遍历 T 中除了 K 的键。
type MyExclude<T, P> = T extends P ? never : T;
type MyOmit<T, K extends keyof T> = {
[P in MyExclude<keyof T, K>]: T[key]
}
解法二,利用 Pick 实现,获取 T 中除了 K 的键
type MyOmit<T, K extends keyof T> = Pick<T, Exclude<keyof T, K>>
解法三,直接实现
一开始我是这样想的,但是这样不对,因为不应该存在的键也会存在,只不过类型为 never。
type MyOmit<T, K extends keyof T> = {
[P in keyof T]: P extends K ? never : T[key]
}
要将对应的键设置为 never
type MyOmit<T, K extends keyof T> = {
[P in keyof T as P extends K ? never : P]: T[P];
};
注意这里的 as 其实是这样的 [P in keyof T as (P extends K ? never : P)]: T[A],as 后面的 P 和前面的命名必须是一样的。
16. Medium - 8 - Readonly 2 对象部分属性只读
MyReadonly2<T, K> 在 T 把 K 包含的属性置为 readonly 其余不变,不传 K 就把所有的属性变成 readonly,此时同 Readonly。
重点是 K 可以不传,所以要赋默认值。
从 medium 题目开始,大家可以慢慢地把不同的类型结合起来使用了,一句话写还是比较难。
type MyReadonly2<T, K extends keyof T =
keyof T> = Readonly<Pick<T, K>> & Omit<T, K>
17. Medium - 9 - Deep Readonly 对象属性只读(递归)※
题目要求:实现一个泛型 DeepReadonly<T>,它将对象的每个参数及其子对象递归地设为只读。
type DeepReadonly<T> = {
readonly [P in keyof T]: keyof T[P] extends never ? T[P] : DeepReadonly<T[P]>;
};
这道题目最难的就是,如何判断一个类型是否需要继续递归只读,而这里最精彩的地方就是关于 never 的应用: keyof T extends never 表示检查 T 是否有可枚举的键。接下来问题就简单多了,如果是存在就递归调用 DeepReadonly 否则直接返回当前类型。
关于 never 的应用其实有很多,在这篇文章中,我曾经总结过一些 《从 Vue3 源码中了解你所不知道的 never》
浙公网安备 33010602011771号