本文面向已经会写基础 TypeScript 的前端/全栈开发者,系统梳理类型体操常见的 100 个知识点。可直接复制到
.ts文件中验证。
一、类型体操是什么
类型体操不是为了炫技,而是利用 TypeScript 的类型系统在编译期完成约束、推导、转换和校验。它常见于工具类型、组件库 Props 推导、接口返回值建模、路由参数解析、表单 schema 推导、状态管理类型收窄等场景。
二、基础认知:1-15
1. 类型和值是两个世界
TypeScript 的类型只存在于编译期,运行时不会保留大多数类型信息。
ts
type User = { name: string };
const user: User = { name: 'Tom' };
User 是类型,user 是值。类型体操操作的是类型世界。
2. type 适合表达复杂类型
type 可以定义联合类型、交叉类型、条件类型、映射类型等。
ts
type ID = string | number;
type Result<T> = T extends Error ? false : true;
类型体操通常以 type 为主要载体。
3. interface 适合对象结构扩展
interface 支持声明合并和继承,更适合公开 API 对象形状。
ts
interface User {
name: string;
}
interface User {
age: number;
}
合并后 User 同时包含 name 和 age。
4. 字面量类型是精确类型
ts
type Method = 'GET' | 'POST';
字面量类型比 string 更精确,是类型体操判断分支的重要材料。
5. 联合类型表示"或"
ts
type Status = 'loading' | 'success' | 'error';
类型体操经常对联合类型逐项分发处理。
6. 交叉类型表示"合并约束"
ts
type A = { name: string };
type B = { age: number };
type C = A & B;
C 同时需要满足 A 和 B。
7. never 表示不可能
ts
type Impossible = string & number;
Impossible 是 never。它常用于过滤联合类型。
8. unknown 是安全的顶层类型
任何值都能赋给 unknown,但使用前必须收窄。
ts
let value: unknown;
// value.toFixed(); // 报错
类型体操中 unknown 常用于泛型约束的安全兜底。
9. any 会放弃类型检查
ts
type A = any extends string ? 1 : 2;
any 会污染类型推导,复杂工具类型应尽量少用。
10. void 与 undefined 不完全等价
函数返回 void 表示调用者不应该依赖返回值。
ts
type Fn = () => void;
在工具类型里处理函数返回值时需要区分 void、undefined、never。
11. object 不等于普通对象字面量
object 表示非原始类型,不包含 string、number、boolean 等。
ts
type IsObject<T> = T extends object ? true : false;
函数、数组也满足 object。
12. keyof 获取对象键名联合
ts
type User = { name: string; age: number };
type Keys = keyof User; // 'name' | 'age'
这是对象类型遍历的入口。
13. 索引访问类型读取属性类型
ts
type User = { name: string; age: number };
type Name = User['name'];
也可以读取多个属性:User[keyof User] 得到属性值联合。
14. typeof 从值反推类型
ts
const config = { mode: 'dark', size: 12 };
type Config = typeof config;
注意普通对象字面量会发生类型拓宽。
15. as const 锁定字面量
ts
const routes = ['home', 'about'] as const;
type Route = typeof routes[number]; // 'home' | 'about'
as const 是从运行时配置生成精确类型的常用起点。
三、泛型基础:16-30
16. 泛型是类型层面的参数
ts
type Box<T> = { value: T };
T 像函数参数一样,可以把外部类型传入内部。
17. 泛型约束使用 extends
ts
type Prop<T extends object, K extends keyof T> = T[K];
约束能保证后续类型操作合法。
18. 泛型默认值降低使用成本
ts
type ApiResponse<T = unknown> = {
data: T;
code: number;
};
默认值适合工具类型的可选配置。
19. 泛型可以依赖前一个泛型
ts
type PickValue<T, K extends keyof T = keyof T> = T[K];
后面的泛型可以使用前面泛型推导出的结果。
20. 泛型推导来自调用位置
ts
function identity<T>(value: T): T {
return value;
}
const result = identity('hello'); // string
函数泛型一般由参数自动推导。
21. 泛型推导会尽量选择宽类型
ts
function wrap<T>(value: T) {
return { value };
}
const a = wrap('x'); // { value: string }
如果需要字面量类型,可结合 as const 或 const 泛型。
22. const 泛型保留字面量信息
ts
function tuple<const T extends readonly unknown[]>(value: T): T {
return value;
}
const t = tuple(['a', 'b']); // readonly ['a', 'b']
这是 TS 5.x 中非常实用的推导增强。
23. 泛型约束不等于泛型结果
ts
type GetLength<T extends { length: number }> = T['length'];
T 只是被限制有 length,但仍保留传入类型本身的信息。
24. 多泛型之间可以建立关系
ts
type Assign<T extends object, U extends object> = T & U;
复杂类型体操本质是建立多个类型参数之间的推导关系。
25. 泛型可以用于类型函数
ts
type ToArray<T> = T[];
类型体操里的 type 可以理解为"类型函数"。
26. 泛型分发依赖裸类型参数
ts
type ToArray<T> = T extends unknown ? T[] : never;
type R = ToArray<string | number>; // string[] | number[]
裸 T 出现在 extends 左侧时会触发联合类型分发。
27. 包裹泛型可阻止分发
ts
type ToArrayNoDistribute<T> = [T] extends [unknown] ? T[] : never;
type R = ToArrayNoDistribute<string | number>; // (string | number)[]
这是控制条件类型行为的关键技巧。
28. 泛型也能用于类和接口
ts
interface Store<State> {
getState(): State;
}
类型体操不局限于 type,只是 type 更灵活。
29. 泛型需要避免过度抽象
如果某个类型只用一次,且没有复用价值,直接写具体类型更清晰。
ts
type UserName = string;
这种别名如果没有语义价值,可能只是噪音。
30. 泛型命名要表达含义
常见约定:T 表示通用类型,K 表示键,V 表示值,R 表示返回值。
ts
type ValueOf<T> = T[keyof T];
复杂场景可以使用 Source、Target、Options 等更清晰的名字。
四、条件类型与推导:31-45
31. 条件类型是类型层面的 if
ts
type IsString<T> = T extends string ? true : false;
它根据类型兼容关系返回不同结果。
32. extends 判断的是可赋值性
ts
type A = 'x' extends string ? true : false; // true
type B = string extends 'x' ? true : false; // false
不是"相等",而是"是否能赋值给"。
33. 条件类型可以嵌套
ts
type TypeName<T> = T extends string
? 'string'
: T extends number
? 'number'
: 'other';
嵌套过深会降低可读性,必要时拆分。
34. infer 用于在条件类型中声明待推导变量
ts
type Return<T> = T extends (...args: any[]) => infer R ? R : never;
R 由函数返回值位置推导得出。
35. infer 可以推导数组元素
ts
type Element<T> = T extends Array<infer Item> ? Item : never;
Element<string[]> 得到 string。
36. infer 可以推导 Promise 结果
ts
type UnwrapPromise<T> = T extends Promise<infer R> ? R : T;
实际项目中常用于 API 返回值处理。
37. infer 可以推导函数参数
ts
type FirstArg<T> = T extends (arg: infer A, ...rest: any[]) => any ? A : never;
可以提取回调函数的参数类型。
38. 多个 infer 可以同时存在
ts
type FnParts<T> = T extends (...args: infer P) => infer R ? [P, R] : never;
同时获得参数元组和返回值。
39. infer 可以加约束
ts
type FirstString<T> = T extends [infer F extends string, ...unknown[]] ? F : never;
推导变量本身也能被限制。
40. 条件类型能过滤联合类型
ts
type OnlyString<T> = T extends string ? T : never;
type R = OnlyString<string | number | boolean>; // string
never 在联合类型中会被自动消除。
41. 条件类型能排除类型
ts
type MyExclude<T, U> = T extends U ? never : T;
这就是内置 Exclude 的核心思想。
42. 条件类型能提取类型
ts
type MyExtract<T, U> = T extends U ? T : never;
这就是内置 Extract 的核心思想。
43. 判断 never 需要阻止分发
ts
type IsNever<T> = [T] extends [never] ? true : false;
如果直接写 T extends never,传入 never 会得到 never,不是 true。
44. 判断 any 需要特殊技巧
ts
type IsAny<T> = 0 extends (1 & T) ? true : false;
any 与交叉类型结合会产生特殊兼容性,可用于识别。
45. 判断 unknown
ts
type IsUnknown<T> = unknown extends T
? IsAny<T> extends true
? false
: true
: false;
判断 unknown 时通常要先排除 any。
五、联合类型技巧:46-55
46. 联合类型会被自动扁平化
ts
type A = string | (number | boolean);
最终等价于 string | number | boolean。
47. never 会从联合类型中消失
ts
type A = string | never; // string
这使得 never 成为过滤分支的理想返回值。
48. 联合类型转交叉类型
ts
type UnionToIntersection<U> = (
U extends unknown ? (arg: U) => void : never
) extends (arg: infer I) => void
? I
: never;
利用函数参数逆变位置完成转换。
49. 获取联合类型最后一项
ts
type LastOfUnion<U> = UnionToIntersection<
U extends unknown ? () => U : never
> extends () => infer R
? R
: never;
联合类型本身无顺序,所谓"最后一项"是编译器内部表现,不应依赖业务语义。
50. 联合转元组属于高级技巧
ts
type UnionToTuple<U, Last = LastOfUnion<U>> = [U] extends [never]
? []
: [...UnionToTuple<Exclude<U, Last>>, Last];
它常用于类型挑战,但生产代码中要谨慎使用。
51. 可辨识联合类型适合状态建模
ts
type State =
| { type: 'loading' }
| { type: 'success'; data: string }
| { type: 'error'; message: string };
通过共同字段 type 进行安全收窄。
52. 联合类型收窄依赖控制流分析
ts
function handle(state: State) {
if (state.type === 'success') {
state.data;
}
}
类型系统会根据判断条件缩小类型。
53. 穷尽检查使用 never
ts
function assertNever(value: never): never {
throw new Error(String(value));
}
当新增联合分支却没有处理时,never 检查会报错。
54. 联合类型分发可实现批量转换
ts
type EventName<T> = T extends string ? `on${Capitalize<T>}` : never;
type R = EventName<'click' | 'focus'>; // 'onClick' | 'onFocus'
模板字符串类型与联合分发结合非常强大。
55. 联合过大可能影响编译性能
大量模板字符串交叉组合会产生指数级联合类型。
ts
type Size = 'sm' | 'md' | 'lg';
type Color = 'red' | 'blue' | 'green';
type ClassName = `${Size}-${Color}`;
组合规模要控制,避免类型爆炸。
六、映射类型与对象变换:56-70
56. 映射类型遍历键名
ts
type Copy<T> = {
[K in keyof T]: T[K];
};
这是对象类型转换的基础形式。
57. readonly 修饰符可添加
ts
type MyReadonly<T> = {
readonly [K in keyof T]: T[K];
};
对应内置 Readonly。
58. readonly 修饰符可移除
ts
type Mutable<T> = {
-readonly [K in keyof T]: T[K];
};
-readonly 表示去掉只读。
59. 可选修饰符可添加
ts
type MyPartial<T> = {
[K in keyof T]?: T[K];
};
对应内置 Partial。
60. 可选修饰符可移除
ts
type RequiredProps<T> = {
[K in keyof T]-?: T[K];
};
-? 表示去掉可选。
61. Pick 选择部分键
ts
type MyPick<T, K extends keyof T> = {
[P in K]: T[P];
};
这是最常用的对象裁剪工具。
62. Omit 排除部分键
ts
type MyOmit<T, K extends keyof any> = Pick<T, Exclude<keyof T, K>>;
先排除键,再 Pick 剩余键。
63. Record 创建键值映射
ts
type MyRecord<K extends keyof any, V> = {
[P in K]: V;
};
keyof any 等价于 string | number | symbol。
64. 键名重映射使用 as
ts
type Prefix<T> = {
[K in keyof T as `get${Capitalize<string & K>}`]: () => T[K];
};
可把 { name: string } 变成 { getName: () => string }。
65. 键名重映射可过滤属性
ts
type OnlyStringProps<T> = {
[K in keyof T as T[K] extends string ? K : never]: T[K];
};
映射到 never 的键会被删除。
66. 对象值类型可统一转换
ts
type Nullable<T> = {
[K in keyof T]: T[K] | null;
};
适合表单初始值、接口字段兼容等场景。
67. 深度递归对象转换
ts
type DeepReadonly<T> = T extends (...args: any[]) => any
? T
: T extends object
? { readonly [K in keyof T]: DeepReadonly<T[K]> }
: T;
递归时要注意函数、数组、特殊对象的处理。
68. 深度可选类型
ts
type DeepPartial<T> = T extends object
? { [K in keyof T]?: DeepPartial<T[K]> }
: T;
常用于配置覆盖和测试数据构造。
69. 获取可选键
ts
type OptionalKeys<T> = {
[K in keyof T]-?: {} extends Pick<T, K> ? K : never;
}[keyof T];
通过 {} 是否能赋值给单属性对象判断可选性。
70. 获取必选键
ts
type RequiredKeys<T> = Exclude<keyof T, OptionalKeys<T>>;
复用 OptionalKeys 能保持实现简洁。
七、元组与数组:71-82
71. 数组元素类型提取
ts
type ArrayItem<T> = T extends readonly (infer Item)[] ? Item : never;
使用 readonly 可兼容普通数组和只读数组。
72. 元组长度是字面量数字
ts
type T = [string, number];
type Len = T['length']; // 2
普通数组的 length 是 number,元组更精确。
73. 提取元组第一项
ts
type First<T extends readonly unknown[]> = T extends readonly [infer F, ...unknown[]]
? F
: never;
变长元组是类型体操核心能力之一。
74. 提取元组最后一项
ts
type Last<T extends readonly unknown[]> = T extends readonly [...unknown[], infer L]
? L
: never;
从尾部推导需要使用剩余元素语法。
75. 去掉第一项
ts
type Shift<T extends readonly unknown[]> = T extends readonly [unknown, ...infer Rest]
? Rest
: [];
常用于递归处理参数列表。
76. 去掉最后一项
ts
type Pop<T extends readonly unknown[]> = T extends readonly [...infer Rest, unknown]
? Rest
: [];
与 Last 的推导位置类似。
77. 元组追加元素
ts
type Push<T extends readonly unknown[], V> = [...T, V];
如果输入是只读元组,输出默认是可变元组,可按需加 readonly。
78. 元组前置元素
ts
type Unshift<T extends readonly unknown[], V> = [V, ...T];
常用于递归构造结果。
79. 元组递归反转
ts
type Reverse<T extends readonly unknown[], R extends unknown[] = []> = T extends readonly [
infer F,
...infer Rest
]
? Reverse<Rest, [F, ...R]>
: R;
第二个泛型 R 是累加器。
80. 元组构造数字计数
ts
type BuildTuple<N extends number, R extends unknown[] = []> = R['length'] extends N
? R
: BuildTuple<N, [...R, unknown]>;
类型层面没有直接算术,常用元组长度模拟数字。
81. 类型层面加法
ts
type Add<A extends number, B extends number> = [
...BuildTuple<A>,
...BuildTuple<B>
]['length'];
适合小数字,过大数字会触发递归深度限制。
82. 判断数组和元组
ts
type IsTuple<T extends readonly unknown[]> = number extends T['length'] ? false : true;
普通数组长度是 number,元组长度是数字字面量。
八、模板字符串类型:83-90
83. 模板字符串类型可拼接字符串
ts
type Event<K extends string> = `on${Capitalize<K>}`;
type Click = Event<'click'>; // 'onClick'
它让字符串协议也能被类型系统约束。
84. 内置大小写工具
ts
type A = Uppercase<'abc'>; // 'ABC'
type B = Lowercase<'ABC'>; // 'abc'
type C = Capitalize<'name'>; // 'Name'
type D = Uncapitalize<'Name'>; // 'name'
这些工具只能处理字符串字面量类型。
85. 解析路径参数
ts
type Params<Path extends string> = Path extends `${string}:${infer Param}/${infer Rest}`
? Param | Params<Rest>
: Path extends `${string}:${infer Param}`
? Param
: never;
type P = Params<'/user/:id/post/:postId'>; // 'id' | 'postId'
路由库中经常使用此类技巧。
86. 字符串递归替换
ts
type ReplaceAll<S extends string, From extends string, To extends string> = From extends ''
? S
: S extends `${infer Head}${From}${infer Tail}`
? `${Head}${To}${ReplaceAll<Tail, From, To>}`
: S;
要特别处理 From 为空字符串,否则会无限递归。
87. 字符串转联合字符
ts
type StringToUnion<S extends string> = S extends `${infer C}${infer Rest}`
? C | StringToUnion<Rest>
: never;
适合理解递归,不建议在长字符串上滥用。
88. 去除左侧空白
ts
type Space = ' ' | '\n' | '\t';
type TrimLeft<S extends string> = S extends `${Space}${infer Rest}` ? TrimLeft<Rest> : S;
字符串递归通常由"匹配一段 + 处理剩余"组成。
89. 去除右侧空白
ts
type TrimRight<S extends string> = S extends `${infer Rest}${Space}` ? TrimRight<Rest> : S;
左右组合即可得到完整 Trim。
90. 字符串协议映射对象键
ts
type Watcher<T> = {
[K in keyof T as `${string & K}Changed`]: (value: T[K]) => void;
};
可以把数据字段自动映射成事件名。
九、函数、this 与重载:91-96
91. 获取函数参数类型
ts
type MyParameters<T extends (...args: any[]) => any> = T extends (...args: infer P) => any
? P
: never;
对应内置 Parameters。
92. 获取函数返回值类型
ts
type MyReturnType<T extends (...args: any[]) => any> = T extends (...args: any[]) => infer R
? R
: never;
对应内置 ReturnType。
93. 获取构造函数参数
ts
type MyConstructorParameters<T extends abstract new (...args: any) => any> = T extends abstract new (
...args: infer P
) => any
? P
: never;
abstract new 可兼容抽象类构造签名。
94. 获取实例类型
ts
type MyInstanceType<T extends abstract new (...args: any) => any> = T extends abstract new (
...args: any
) => infer R
? R
: never;
对应内置 InstanceType。
95. 提取 this 参数
ts
type MyThisParameterType<T> = T extends (this: infer This, ...args: any[]) => any
? This
: unknown;
TypeScript 允许函数类型显式声明 this 参数。
96. 移除 this 参数
ts
type MyOmitThisParameter<T> = unknown extends MyThisParameterType<T>
? T
: T extends (this: any, ...args: infer P) => infer R
? (...args: P) => R
: T;
适合把依赖 this 的函数转换为普通函数签名。
十、实战思维与边界:97-100
97. 类型递归有深度限制
复杂递归类型会触发"Type instantiation is excessively deep and possibly infinite"。
ts
type Repeat<T extends unknown[]> = Repeat<[...T, unknown]>;
设计递归类型时必须有明确终止条件。
98. 类型体操要服务可读性
ts
type ReadableUser = Pick<User, 'id' | 'name'>;
比起写一个过度通用的深层工具,清晰表达业务意图更重要。
99. 优先复用内置工具类型
常用内置工具包括:Partial、Required、Readonly、Pick、Omit、Record、Exclude、Extract、NonNullable、Parameters、ReturnType、Awaited。
ts
type Data = Awaited<Promise<string>>; // string
内置工具经过社区验证,通常比手写更可靠。
100. 类型测试能提高可靠性
ts
type Equal<A, B> = (<T>() => T extends A ? 1 : 2) extends <T>() => T extends B ? 1 : 2
? true
: false;
type Expect<T extends true> = T;
type Case = Expect<Equal<ReturnType<() => string>, string>>;
复杂工具类型建议配套类型测试,避免后续修改破坏推导结果。
十一、常见类型体操流程图
条件类型分发流程
对象属性过滤流程
元组递归处理流程
字符串递归解析流程
十二、学习建议
- 先熟悉
keyof、索引访问、泛型、条件类型、映射类型。 - 再练习
infer、联合分发、模板字符串类型和元组递归。 - 每写一个复杂工具类型,都加 2 到 5 个类型测试用例。
- 遇到难题先画"输入类型 -> 判断条件 -> 推导位置 -> 输出类型"的流程。
- 生产代码以可维护为第一原则,能用内置工具就不要重复造轮子。