TypeScript 中编写复杂类型运算的原理(递归字符串、数组、对象)

213 阅读4分钟

TypeScript 中编写复杂类型运算的原理,也就是常说的类型体操。刷了大量的体操类型题后发现规律就是:递归处理字符串、递归处理数组、递归处理对象。三种场景以下是对 TypeScript 中编写复杂类型运算的原理、思路及典型场景的深度整理,涵盖递归处理字符串、数组、对象的核心逻辑和最佳实践:


一、类型运算的核心原理

TypeScript 类型系统本质上是 结构化类型系统,通过 条件类型(Conditional Types)映射类型(Mapped Types)模板字面量类型(Template Literal Types) 实现类型运算。递归类型的关键在于 自引用(Self-Reference)终止条件(Base Case)


二、递归处理字符串

场景

  • 需要动态解析字符串结构(如路由参数、模板字符串)
  • 实现字符串分割、替换、模式匹配等操作

原理与思路

  1. 递归拆分:利用 infer 提取部分字符串
  2. 模板字面量:结合 ${infer Head}${Delimiter}${infer Tail} 分割字符串
  3. 终止条件:当无法继续分割时返回结果

示例:

  • 解析路径参数
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!'

三、递归处理数组

场景

  • 实现数组翻转、过滤、扁平化等操作
  • 处理元组类型转换

原理与思路

  1. 递归首尾分解[infer Head, ...infer Tail] 模式
  2. 累加器模式:通过第二个泛型参数传递中间结果
  3. 终止条件:空数组时返回结果

示例:

  • 数组扁平化
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 转换
  • 展开嵌套对象结构
  • 过滤特定属性类型

原理与思路

  1. 映射类型遍历{ [K in keyof T]: ... }
  2. 条件类型判断:根据当前属性类型决定是否递归
  3. 终止条件:遇到非对象类型时停止递归

示例:

-深度展开嵌套对象

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"

六、调试技巧

  1. 使用 类型测试 验证结果:
type Assert<T, Expected> = T extends Expected ? true : false;
type Test = Assert<Flatten<[1, [2]]>, [1, 2]>; // true
  1. 分阶段推导:
// 步骤1:定义中间类型
type Step1 = NestedObj['b'];
// => { c: { d: string } }

// 步骤2:递归下一层
type Step2 = Expand<Step1>;
// => { c: { d: string } }

通过系统化理解这些模式,可以构建出类型安全的复杂类型系统,同时保持代码可维护性。核心要点:明确递归目标、设计终止条件、优先组合现有工具类型

体操类型挑战:github.com/type-challe…