Skip to content

类型体操(Type Manipulation)


核心问题解析

问题一:能不能让类型自动推导嵌套属性?

我们经常需要从深层嵌套对象中提取类型:

ts
const user = {
  id: 1,
  profile: {
    name: "Alice",
    address: {
      city: "Beijing",
      zip: "100000"
    }
  }
};

// 希望:Get<typeof user, 'profile.address.city'> → string

答案:可以!通过递归类型 + 模板字面量类型实现。


问题二:如何实现类型递归?

递归类型就是“类型引用自身”:

ts
interface TreeNode {
  value: number;
  children: TreeNode[]; // 引用自身
}

在工具类型中,我们也可以写递归:

ts
type DeepReadonly<T> = {
  readonly [K in keyof T]: 
    T[K] extends object ? DeepReadonly<T[K]> : T[K];
};

⚠️ 关键:必须有终止条件(如 T[K] extends object ? ... : T[K]),否则会无限展开。


问题三:字符串字面量类型能操作吗?

TypeScript 4.1+ 支持模板字面量类型,可以拼接、解析字符串类型:

ts
type EventName = `on${Capitalize<string>}`; 
// 如:'onMouseUp', 'onKeyDown'

type Join<A, B> = `${A}.${B}`;
type T = Join<'user', 'profile'>; // 'user.profile'

答案:是的,字符串字面量类型可以参与类型计算,是“类型编程”的高级武器。


学习目标详解

目标一:实现递归类型(注意终止条件)

递归类型常用于处理嵌套结构。

✅ 实现 DeepPartial<T>:深度可选
ts
type DeepPartial<T> = {
  [K in keyof T]?: 
    T[K] extends object 
      ? T[K] extends Function // 避免函数也被递归
        ? T[K] 
        : DeepPartial<T[K]> 
      : T[K];
};

// 测试
interface User {
  id: number;
  profile: {
    name: string;
    contacts: {
      email: string;
    }
  }
}

type PartialUser = DeepPartial<User>;
// profile?: { name?: string; contacts?: { email?: string } }

📌 终止条件

  • T[K] extends object:只对对象递归
  • T[K] extends Function:排除函数,避免无限递归

目标二:使用模板字面量类型拼接字符串类型

1. 基本拼接
ts
type Path<T, U> = `${T}.${U}`;
type T1 = Path<'user', 'profile'>; // 'user.profile'
2. 联合类型分发
ts
type Events = 'click' | 'hover';
type Handler = `${Events}Handler`; 
// 'clickHandler' | 'hoverHandler'
3. 大小写转换(内置)
ts
type T2 = Capitalize<'hello'>;  // 'Hello'
type T3 = Uppercase<'abc'>;     // 'ABC'
type T4 = Uncapitalize<'Hello'>; // 'hello'
type T5 = Lowercase<'ABC'>;     // 'abc'

目标三:掌握类型递归与分布式条件类型

分布式条件类型回顾

T 是联合类型时,T extends U ? X : Y 会自动分发:

ts
type Box<T> = T extends any ? [T] : never;
type T = Box<'a' | 'b'>; // [a] | [b]
结合递归:实现 Flatten<T>
ts
type Flatten<T> = T extends Array<infer Item>
  ? Flatten<Item>  // 递归展开
  : T;               // 终止:非数组直接返回

type T6 = Flatten<number[]>;           // number
type T7 = Flatten<string[][][]>;       // string
type T8 = Flatten<(number | string)[]>; // number | string

目标四:实现 DeepPartialFlattenGet<T, 'a.b.c'> 等高级类型

1. DeepPartial<T>(已实现)

见上文。

2. Flatten<T>(已实现)

见上文。

3. Get<T, P>:路径访问类型

实现类似 lodash.get 的类型安全版本。

ts
type Get<T, P extends string> = 
  P extends `${infer K}.${infer R}`
    ? K extends keyof T
      ? Get<T[K], R>  // 递归访问
      : never
    : P extends keyof T
      ? T[P]
      : never;

// 测试
interface Config {
  api: {
    url: string;
    timeout: number;
  };
  ui: {
    theme: 'light' | 'dark';
  }
}

type ApiUrl = Get<Config, 'api.url'>;     // string
type Theme = Get<Config, 'ui.theme'>;     // 'light' | 'dark'
type Invalid = Get<Config, 'api.port'>;   // never(不存在)

📌 关键技术

  • infer Kinfer R:拆分字符串字面量
  • 递归调用 Get<T[K], R>
  • keyof T 安全检查

总结:本节核心要点

技术用途示例
递归类型处理嵌套结构DeepPartial, Flatten
模板字面量字符串类型计算${A}.${B}, Capitalize
分布式条件类型联合类型自动分发Box<T> = T extends any ? [T] : never
infer 推断提取类型片段infer K, infer R

📌 类型体操的本质是:用类型表达逻辑,让编译器替我们做类型推导


练习题


练习题 1:实现 DeepReadonly<T>

要求所有嵌套属性都变为 readonly,但函数保持原样。

ts
type DeepReadonly<T> = {
  readonly [K in keyof T]: 
    T[K] extends Function 
      ? T[K] 
      : T[K] extends object 
        ? DeepReadonly<T[K]> 
        : T[K];
};

// 测试
interface Nested {
  a: string;
  b: {
    c: number;
    d: () => void;
  };
}

type RO = DeepReadonly<Nested>;
// b: { readonly c: number; d: () => void }

练习题 2:实现 TupleToUnion<T>

将元组类型转为联合类型:

ts
type TupleToUnion<T> = T extends Array<infer U> ? U : never;

type T1 = TupleToUnion<[string, number]>; // string | number

练习题 3:实现 CamelToSnake<T>

将驼峰命名的字符串字面量转为蛇形命名:

ts
type CamelToSnake<S extends string> = 
  S extends `${infer First}${infer R}`
    ? R extends Uncapitalize<R>
      ? `${Lowercase<First>}${CamelToSnake<R>}`
      : `${Lowercase<First>}_${CamelToSnake<R>}`
    : S;

// 更准确的实现(基于大写字母拆分):
type CamelToSnake<S extends string> = 
  S extends `${infer L}${infer M}${infer R}`
    ? M extends Uppercase<M>
      ? `${L}_${Lowercase<M>}${CamelToSnake<R>}`
      : `${L}${CamelToSnake<`${M}${R}`>}`
    : S;

type T2 = CamelToSnake<'userProfile'>; // 'user_profile'
type T3 = CamelToSnake<'APIResponse'>; // 'a_p_i_response'

练习题 4:实现 Merge<T, U>

合并两个对象类型,U 的属性覆盖 T

ts
type Merge<T, U> = Omit<T, keyof U> & U;

type T4 = Merge<{ a: 1; b: 2 }, { b: 3; c: 4 }>;
// { a: 1; b: 3; c: 4 }

练习题 5:实现 GetRecursive<T, Path>

增强版 Get,支持数组索引访问:

ts
type GetRecursive<T, P> = 
  P extends `${infer K}.${infer R}`
    ? K extends keyof T
      ? GetRecursive<T[K], R>
      : never
    : P extends keyof T
      ? T[P]
      : P extends `${number}`
        ? T extends Array<infer Item> ? Item : never
        : never;

// 测试
type Users = { name: string }[];
type T5 = GetRecursive<{ users: Users }, 'users.0.name'>; // string