TypeScript 中编写复杂类型运算的原理,也就是常说的类型体操。刷了大量的体操类型题后发现规律就是:递归处理字符串、递归处理数组、递归处理对象。三种场景以下是对 TypeScript 中编写复杂类型运算的原理、思路及典型场景的深度整理,涵盖递归处理字符串、数组、对象的核心逻辑和最佳实践:
一、类型运算的核心原理
TypeScript 类型系统本质上是 结构化类型系统,通过 条件类型(Conditional Types)、映射类型(Mapped Types) 和 模板字面量类型(Template Literal Types) 实现类型运算。递归类型的关键在于 自引用(Self-Reference) 和 终止条件(Base Case)。
二、递归处理字符串
场景
- 需要动态解析字符串结构(如路由参数、模板字符串)
- 实现字符串分割、替换、模式匹配等操作
原理与思路
- 递归拆分:利用
infer
提取部分字符串 - 模板字面量:结合
${infer Head}${Delimiter}${infer Tail}
分割字符串 - 终止条件:当无法继续分割时返回结果
示例:
- 解析路径参数
type ExtractParams<Path extends string> =
Path extends `${string}:${infer Param}/${infer Rest}`
? Param | ExtractParams<Rest>
: Path extends `${string}:${infer Param}`
? Param
: never;
type Params = ExtractParams<"/user/:id/posts/:postId">; // "id" | "postId"
- 将下划线转换成小驼峰
比如: aa_bb_cc 转换为 aaBbCc :
思路
- 识别
_
关键分割词,左侧递归,右侧递归。 - 将分词结果首字母大写, 得到大驼峰。
- 将大驼峰首字母小写
Uncapitalize
- 识别
type SnakeToPascal<T extends string> = T extends `${infer Left}_${infer Rigth}` ? `${SnakeToPascal<Left>}${SnakeToPascal<Rigth>}` : Capitalize<T>;
type SnakeToCamel<T extends string> = Uncapitalize<SnakeToPascal<T>>;
type snake = 'aa_bb_cc_';
type pascal = SnakeToPascal<snake>
// type pascal = "AaBbCc"
type camel = SnakeToCamel<snake>
// type camel = "aaBbCc"
- 删除字符串中指定字符 比如:
type Butterfly = DropString<'foobar!', 'fb'> // 'ooar!'
思路:
- 将剔除的字符转成单个字符构成的联合类型【如果要实现连续字符串剔除,请参考上一题即可】
- 递归剔除字符继承自第一步构成的联合字符集
// type Butterfly = DropString<'foobar!', 'fb'> // 'ooar!'
// 将字符串转成单个字符构成的联合类型(挑出相同的字符)'fb' --> 'f' | 'b'
type StringToUnion<T extends string> = T extends `${infer L}${infer R}` ? `${L}` | StringToUnion<R> : T;
// 从左->递归
type DropString<S extends string, T extends string> = S extends `${infer L}${infer R}` ?
L extends StringToUnion<T> ? `${DropString<R, T>}` : `${L}${DropString<R, T>}`
:S
type Butterfly = DropString<'foobar!', 'fb'> // 'ooar!'
三、递归处理数组
场景
- 实现数组翻转、过滤、扁平化等操作
- 处理元组类型转换
原理与思路
- 递归首尾分解:
[infer Head, ...infer Tail]
模式 - 累加器模式:通过第二个泛型参数传递中间结果
- 终止条件:空数组时返回结果
示例:
- 数组扁平化
type Flatten<T extends any[]> =
T extends [infer First, ...infer Rest]
? First extends any[]
? [...Flatten<First>, ...Flatten<Rest>]
: [First, ...Flatten<Rest>]
: [];
type Nested = [1, [2, [3, 4], 5]];
type Flat = Flatten<Nested>; // [1, 2, 3, 4, 5]
- 实现 Shift Pop
type Shift<T> = T extends [infer F, ...infer Rest] ? [...Rest] : T;
type arr = Shift<[1,2,3]> // type arr = [2, 3]
type Pop<T> = T extends [...infer F, infer L] ? [...F] : T;
type arr2 = Pop<[1,2,3]>; // type arr2 = [1, 2]
四、递归处理对象
场景
- 深度 Readonly/Partial 转换
- 展开嵌套对象结构
- 过滤特定属性类型
原理与思路
- 映射类型遍历:
{ [K in keyof T]: ... }
- 条件类型判断:根据当前属性类型决定是否递归
- 终止条件:遇到非对象类型时停止递归
示例:
-深度展开嵌套对象
type Expand<T> = T extends object
? T extends infer O
? { [K in keyof O]: Expand<O[K]> }
: never
: T;
type NestedObj = {
a: number;
b: {
c: {
d: string;
};
};
};
type Expanded = Expand<NestedObj>;
/*
{
a: number;
b: {
c: {
d: string;
};
};
}
*/
- 合并多个对象
type Foo = { a: 1; b: 2 }
type Bar = { a: 2 }
type Baz = { c: 3 }
type Result = MergeAll<[Foo, Bar, Baz]> // expected to be { a: 1 | 2; b: 2; c: 3 }
实现:
type Merge<T, M> = {
[k in (keyof T) | (keyof M)]: k extends keyof T
? k extends keyof M
? T[k] | M[k]
: T[k]
: M[k]
}
// 遍历元组,进行合并
type MergeAll<T, R = {}> = T extends [infer I, ...infer Rest]
? MergeAll<Rest, Merge<R, I>> : R;
五、多层嵌套对象展开
高级技巧:路径展开
type Paths<T, Path extends string = ''> =
T extends object
? {
[K in keyof T & string]: Path extends ''
? Paths<T[K], K>
: Paths<T[K], `${Path}.${K}`> | `${Path}.${K}`
}[keyof T & string]
: Path;
type Obj = {
a: {
b: number;
c: {
d: string;
};
};
e: boolean;
};
type AllPaths = Paths<Obj>; // "a" | "a.b" | "a.c" | "a.c.d" | "e"
六、调试技巧
- 使用 类型测试 验证结果:
type Assert<T, Expected> = T extends Expected ? true : false;
type Test = Assert<Flatten<[1, [2]]>, [1, 2]>; // true
- 分阶段推导:
// 步骤1:定义中间类型
type Step1 = NestedObj['b'];
// => { c: { d: string } }
// 步骤2:递归下一层
type Step2 = Expand<Step1>;
// => { c: { d: string } }
通过系统化理解这些模式,可以构建出类型安全的复杂类型系统,同时保持代码可维护性。核心要点:明确递归目标、设计终止条件、优先组合现有工具类型。
体操类型挑战:github.com/type-challe…