引言:为什么要学习类型编程

TypeScript 的类型系统远比大多数人想象的要强大。它不仅仅是用作"标注变量类型"的工具,更是一门完整的编程语言——在类型层面进行编程的语言。掌握了条件类型、映射类型、模板字面量类型等高级特性后,你将能够编写出表达能力极强的类型定义,让编译器帮你捕获大量潜在的运行时错误。

本文将从实用角度出发,深入讲解 TypeScript 类型编程的核心概念和实战技巧,包括条件类型的推断模式、分布式条件类型、模板字面量类型、递归类型定义,以及如何在大型项目中构建健壮的类型层。

一、类型系统的计算本质

1.1 类型就是编译时的程序

TypeScript 的类型层(Type Level)和值层(Value Level)是两个不同的世界。值层的代码在运行时被执行,而类型层的代码在编译时被求解。两者共享相似的语法结构,但运行在不同的阶段。

举个最简单的例子,条件类型 extends ? : 本质上就是 if-else 的分支判断:

type IsString<T> = T extends string ? true : false;
type R1 = IsString<"hello">;   // true
type R2 = IsString<42>;        // false

当 TypeScript 编译器遇到它时,会在编译阶段进行求值,最终得到的类型参与下一次的类型检查。

1.2 从泛型到类型函数

泛型本质上就是类型层面的函数参数。你可以把类型别名+泛型理解为一个"类型函数"——接收类型参数,返回计算结果:

// "类型函数":输入类型 T,返回其 Promise 解包后的类型
type Awaited<T> = T extends Promise<infer U> ? Awaited<U> : T;

type R = Awaited<Promise<Promise<string>>>;  // string

二、条件类型深度解析

2.1 裸类型参数与分布式条件类型

条件类型有一个关键行为——当检查类型是裸的(naked)泛型类型参数时,它会自动"分发"到联合类型的每个成员上。这就是分布式条件类型:

type ToArray<T> = T extends any ? T[] : never;
type R = ToArray<string | number>;  // string[] | number[]

// 不是 (string | number)[]

联合类型 string | number 被分发为 toArray<string> | ToArray<number>。

理解分布式的关键作用:

  • 过滤联合类型:Exclude<T, U> = T extends U ? never : T
  • 拆解嵌套类型:逐成员递归展开
  • 实现穷尽性检查:结合 never 和 never extends 判断遗漏分支

2.2 阻止分布式行为

如果不想让联合类型被分发,用方括号包裹即可:

type ToArrayNoDistribute<T> = [T] extends [any] ? T[] : never;
type R = ToArrayNoDistribute<string | number>;  // (string | number)[]

三、infer 关键字:类型推断的艺术

3.1 infer 的基本用法

infer 允许我们在条件类型中提取子类型,它声明了一个类型变量,编译器会自动推断它的值:

type ReturnType<T> = T extends (...args: any[]) => infer R ? R : never;
type ParamType<T> = T extends (arg: infer P) => any ? P : never;

type Fn = (x: number, y: string) => boolean;
type R = ReturnType<Fn>;   // boolean
type P = ParamType<Fn>;    // number

3.2 多位置 infer

infer 可以出现在同一个条件类型的多个位置,甚至出现在递归类型中:

// 提取数组元素类型
type ElementType<T> = T extends (infer E)[] ? E : never;

// 提取元组的第一个和最后一个元素
type First<T extends any[]> = T extends [infer F, ...any[]] ? F : never;
type Last<T extends any[]> = T extends [...any[], infer L] ? L : never;

type F = First<[1, 2, 3]>;  // 1
type L = Last<[1, 2, 3]>;   // 3

// 提取对象值的联合类型
type ValueOf<T> = T extends Record<string, infer V> ? V : never;

3.3 infer 在字符串操作中的应用

结合模板字面量类型,infer 可以完成强大的字符串解析:

// 提取路由参数
type ExtractParams<T extends string> =
  T extends `{${infer Param}}/${infer Rest}`
    ? Param | ExtractParams<Rest>
    : T extends `{${infer Param}}`
      ? Param
      : never;

type Params = ExtractParams<"/user/{id}/post/{postId}">;  // "id" | "postId"

四、模板字面量类型:字符串的类型级编程

4.1 基本拼接

TypeScript 4.1 引入的模板字面量类型允许我们在类型级别构造字符串:

type Greeting = `Hello, ${string}!`;
type World: "Hello, World!" extends Greeting ? true : false; // true

type EventName = `on${Capitalize<string>}`;
type E: "onClick" extends EventName ? true : false;     // true

4.2 实战:事件映射生成

根据 props 定义自动生成事件回调类型:

type Props = {
  value: string;
  disabled: boolean;
  size: 'small' | 'large';
};

// 生成 onValueChange, onDisabledChange, onSizeChange
type ChangeEvents<T extends Record<string, any>> = {
  [K in keyof T as `on${Capitalize<string & K>}Change`]: {
    value: T[K];
    prevValue: T[K];
  };
};

type Events = ChangeEvents<Props>;
// {
//   onValueChange: { value: string; prevValue: string };
//   onDisabledChange: { value: boolean; prevValue: boolean };
//   onSizeChange: { value: 'small' | 'large'; prevValue: 'small' | 'large' };
// }

4.3 字符串类型体操经典

// 实现 CamelCase:kebab/camelCase → CamelCase
type CamelCase<S extends string> =
  S extends `${infer Head}-${infer Tail}`
    ? `${Head}${CamelCase<Capitalize<Tail>>}`
    : S;

type R1 = CamelCase<"hello-world-test">;  // "helloWorldTest"
type R2 = CamelCase<"foo">;              // "foo"

// 实现 SnakeCase → CamelCase
type SnakeToCamel<S extends string> =
  S extends `${infer Head}_${infer Tail}`
    ? `${Head}${SnakeToCamel<Capitalize<Tail>>}`
    : S;

type R3 = SnakeToCamel<"user_first_name">;  // "userFirstName"

五、映射类型的进阶技巧

5.1 键名重映射(as 子句)

TypeScript 4.1+ 支持在映射类型中使用 as 子句对键名进行转换:

type Getter<T> = {
  [K in keyof T as `get${Capitalize<string & K>}`]: () => T[K];
};

type Setter<T> = {
  [K in keyof T as `set${Capitalize<string & K>}`]: (value: T[K]) => void;
};

interface User {
  name: string;
  age: number;
}

type UserGetters = Getter<User>;
// { getName: () => string; getAge: () => number; }

type UserSetters = Setter<User>;
// { setName: (value: string) => void; setAge: (value: number) => void; }

5.2 键名过滤

利用 as 子句可以过滤掉不符合条件的键:

// 只保留函数类型的属性
type MethodsOf<T> = {
  [K in keyof T as T[K] extends (...args: any[]) => any ? K : never]: T[K];
};

interface Component {
  name: string;
  render: () => void;
  mount: (el: HTMLElement) => void;
  version: number;
}

type MC = MethodsOf<Component>;
// { render: () => void; mount: (el: HTMLElement) => void; }

5.3 深度 Readonly

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

type Config = DeepReadonly<{
  server: { port: number; ssl: { enabled: boolean } };
}>;

// server, port, ssl, enabled 全部变为 readonly

六、实战类型体操:从业务场景出发

6.1 API Response 类型推导

从 API 配置推导响应类型,实现端到端类型安全:

type APIConfig = {
  "/user/{id}": {
    GET: { response: { name: string; age: number } };
    POST: { body: { name: string }; response: { id: string } };
  };
  "/posts": {
    GET: { response: { id: number; title: string }[] };
  };
};

// 提取所有 GET 请求路径
type GetPaths<T> = {
  [K in keyof T]: T[K] extends { GET: any } ? K : never;
}[keyof T];

type AllGetPaths = GetPaths<APIConfig>;  // "/user/{id}" | "/posts"

// 根据路径和方法推导响应类型
type ResponseType<
  Path extends keyof APIConfig,
  Method extends keyof APIConfig[Path]
> = APIConfig[Path][Method] extends { response: infer R } ? R : never;

type UserResponse = ResponseType<"/user/{id}", "GET">;
// { name: string; age: number }

6.2 类型安全的 Redux Action Creator

interface ActionRegistry {
  ADD_TODO: { payload: { text: string }; meta: { timestamp: number } };
  REMOVE_TODO: { payload: { id: string } };
  TOGGLE_TODO: { payload: { id: string } };
}

// 类型安全的 dispatch
type ActionCreators = {
  [K in keyof ActionRegistry as K extends string
    ? Uncapitalize<K>
    : never]: ActionRegistry[K] extends { payload: infer P; meta: infer M }
    ? (payload: P, meta?: M) =>void
    : ActionRegistry[K] extends { payload: infer P }
      ? (payload: P) => void
      : () => void;
};

// ActionCreators 自动推断出所有 action creator 的完整签名
const actions: ActionCreators = {} as any;
actions.addTodo({ text: "Learn TS" }, { timestamp: Date.now() });
actions.removeTodo({ id: "123" });

6.3 类型安全的 ORM 查询构建器

type Model = {
  users: { id: number; name: string; email: string; role: 'admin' | 'user' };
  posts: { id: number; title: string; userId: number };
};

type WhereClause<M> = {
  [K in keyof M]?: M[K] extends string
    ? M[K] | { contains: string; startsWith?: string }
    : M[K] extends number
      ? M[K] | { gt?: number; lt?: number; gte?: number }
      : M[K];
};

type QueryBuilder<T extends keyof Model> = {
  where: (clause: WhereClause<Model[T]>) => QueryBuilder<T>;
  select: <K extends keyof Model[T]>(...fields: K[]) => {
    first: () => Pick<Model[T], K> | undefined;
    all: () => Pick<Model[T], K>[];
  };
};

七、类型体操实战:五个经典问题

7. 实现 GetReturnType:获取函数类型的返回值

type GetReturnType<T> = T extends (...args: any[]) => infer R ? R : never;

type Case1 = GetReturnType<() => string>;              // string
type Case2 = GetReturnType<(x: number) => Promise<void>>; // Promise<void>
type Case3 = GetReturnType<42>;                         // never

7.2 实现 DeepPartial:递归 Partial

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

type UserProfile = DeepPartial<{
  name: string;
  settings: { theme: string; notifications: { email: boolean } };
}>;
// name, settings, theme, notifications, email 全部变为可选

7.3 实现 UnionToIntersection:联合类型的交集转换

type UnionToIntersection<U> =
  (U extends any ? (arg: U) => void : never) extends
  (arg: infer I) => void ? I : never;

// { a: 1 } | { b: 2 } → { a: 1 } & { b: 2 }
type R = UnionToIntersection<{ a: 1 } | { b: 2 }>;  // { a: 1 } & { b: 2 }

原理:利用逆变位置(函数参数)的联合反转行为。(A) => void | (B) => void 可以赋值给 (A | B) => void,但反过来不行。而条件类型分发让 U extends any ? (arg: U) => void : never 生成了联合类型的重载函数,再通过 infer 提取出参数类型的交集。

7.4 实现 IsNever:判断是否为 never 类型

type IsNever<T> = [T] extends [never] ? true : false;
type R1 = IsNever<never>;   // true
type R2 = IsNever<42>;      // false

注意必须用 [T] 包裹来阻止分布式。如果使用裸参数 T extends never,当 T = never 时,空联合中没有任何成员可以分发,结果是 never 本身而非 true。

7.5 实现 ExtractPromise:递归提取嵌套 Promise

type ExtractPromise<T> = T extends Promise<infer U>
  ? ExtractPromise<U>
  : T;

type R = ExtractPromise<Promise<Promise<Promise<string>>>>;  // string

八、类型体操的适用边界

8.1 什么时候该写复杂类型

  • 框架/库开发:需要对外暴露灵活的泛型 API
  • 大型项目核心类型层:公共模块的类型定义
  • 自动化工具生成类型:根据 schema 自动生成类型

8.2 什么时候不该写复杂类型

  • 一次性业务逻辑的类型标注——普通的 interface 和类型别名足够
  • 团队 TypeScript 水平参差不齐时——过度的类型体操会降低维护性
  • 类型计算影响编译性能时——递归深度过高会导致编译器卡顿

8.3 四个最佳实践原则

  1. 可读性优先:注释复杂类型,使用有意义的名字如 PathParams<Route> 而非 P<R>
  2. 逐步构建:将复杂类型拆解为中间类型,每一步类型只做一件事
  3. 避免过度抽象:当类型比对应的 JS 代码还长时,考虑是否真的需要
  4. 使用内置工具类型:Awaited、Partial、Required、Readonly、Record、Pick、Omit、NonNullable 等是首选

九、TypeScript 5.x 新特性速览

9.1 标准装饰器

TypeScript 5.0 实现了 TC39 装饰器提案,与旧版实验性装饰器不兼容但更标准:

function logged<This, Args extends any[], Return>
  (target: (this: This, ...args: Args) => Return, context: ClassMethodDecoratorContext) {
  return function (this: This, ...args: Args): Return {
    console.log(`Calling ${String(context.name)}`);
    return target.call(this, ...args);
  };
}

class ApiService {
  @logged
  fetchData(url: string) { /* ... */ }
}

9.2 特性亮点

  • TypeScript 5.1:轻松实现返回值的重新赋值 + unknown 的 forEach 回调
  • TypeScript 5.2:using 声明(资源管理)、命名与匿名元组推断
  • TypeScript 5.3:额外的细粒度导入检查、import attributes
  • TypeScript 5.4:NoInfer<T> 工具类型防止隐式推断
  • TypeScript 5.5:推断的控制流窄化、隔离声明

9.3 5.x 对类型体操的影响

NoInfer<T> 特别重要——它允许我们将某个类型参数"冻结",防止编译器从参数中推断它:

function createTable<T extends string>(
  items: T[],
  includeId?: NoInfer<T>[]  // T 不会从 includeId 推断
) { /* ... */ }

createTable(["a", "b", "c"], ["a"]);  // T = "a" | "b" | "c"

总结

TypeScript 的类型编程能力是它在前端生态中不可替代的核心竞争力。从条件类型的推断模式到模板字面量类型的字符串解析,从映射类型的键名重映射到逆变位置的交集提取,每个特性都有其独特的应用场景。

关键不在于你掌握了多少类型体操技巧,而在于你能否在合适的场景选择合适的工具。类型最终是为业务服务的——当类型定义能帮你捕获 80% 的逻辑错误,同时保持可维护性时,就是最好的类型层。

点赞(0) 打赏

评论列表 共有 0 条评论

暂无评论
立即
投稿

微信公众账号

微信扫一扫加关注

发表
评论
返回
顶部
0.360730s