ToolkitX
知识库工具箱

高级类型

联合类型、交叉类型、条件类型、映射类型

30min·高级

01. 联合类型的分布式条件类型

条件类型遇到联合类型时有特殊行为——它会自动分布到联合类型的每个成员上。这叫分布式条件类型。 T extends U ? X : Y 当 T 是 A | B 时,等于 (A extends U ? X : Y) | (B extends U ? X : Y)。 这个特性让 Exclude、Extract 等内置类型变得可能:type Exclude<T, U> = T extends U ? never : T。对联合类型 T 的每个成员单独判断,排除匹配 U 的。 如果不想让联合类型分布,把 T 包成一个元组:[T] extends [U] ? X : Y。这样整个联合类型作为一个整体来比较,不拆分。
typescript
// 分布式条件类型
type IsArray<T> = T extends any[] ? 'array' : 'not array';
type R1 = IsArray<string | number[]>;  // 'not array' | 'array'
// 注意!不是 'not array',而是每个成员分别判断

// 阻止分布——包成元组
type IsArraySafe<T> = [T] extends [any[]] ? 'array' : 'not array';
type R2 = IsArraySafe<string | number[]>;  // 'not array'
// 整个联合类型作为整体判断

// Exclude 就是因为分布式才工作
type Status = 'active' | 'inactive' | 'deleted';
type WithoutDeleted = Exclude<Status, 'deleted'>;  // 'active' | 'inactive'
分布式条件类型是 TypeScript 中最容易被误解的特性之一。它像 map 一样对联合类型的每个成员执行条件类型。

02. infer——类型推导的魔法

infer 让你在条件类型里声明一个类型变量,让 TS 去推导它。语法:T extends SomePattern<infer R> ? R : never。 infer 能做很多看似不可能的事: - 提取函数返回值类型(ReturnType) - 提取 Promise resolve 的类型(Awaited) - 提取数组元素类型 - 提取函数参数类型元组(Parameters) - 提取类的实例类型和构造函数参数 infer 可以出现在协变位置(输出位置,如返回值)和逆变位置(输入位置,如参数),推导规则不同。 多个 infer——TS 4.7+ 支持在同一个条件类型里写多个 infer 变量。
typescript
// 提取函数参数类型
  
type MyParameters<T> = T extends (...args: infer P) => any ? P : never;
type Fn = (name: string, age: number) => void;
type Params = MyParameters<Fn>;  // [string, number]

// 提取数组元素
  
type ArrayItem<T> = T extends Array<infer U> ? U : never;
type Item = ArrayItem<string[]>;  // string

// 提取 Promise 值(递归解包)
type Awaited<T> = T extends Promise<infer U> ? Awaited<U> : T;
type P = Awaited<Promise<Promise<number>>>;  // number

// 提取构造函数参数
type ConstructorParams<T> = T extends new (...args: infer P) => any ? P : never;
class User { constructor(name: string, age: number) {} }
type UserParams = ConstructorParams<typeof User>;  // [string, number]

03. 模板字面量类型

TS 4.1 引入了模板字面量类型——把字符串模板的能力带到类型系统里。 基本语法:type Greeting = `Hello, ${string}`。这不仅能做简单的字符串拼接,还跟联合类型交叉产生笛卡尔积。 事件名生成:type EventName<T extends string> = `on${Capitalize<T>}`。 CSS 属性名:type CSSProp = `--${string}`。 路由类型:type Route = `/${string}/${string}`。 内置字符串操作类型:Uppercase<T>、Lowercase<T>、Capitalize<T>、Uncapitalize<T>。 这在写库的类型定义时特别有用——比如给 DOM 事件类型精确映射。
typescript
// 模板字面量 + 联合类型 = 笛卡尔积
type Event = 'click' | 'focus' | 'blur';
type Handler = `on${Capitalize<Event>}`;
// 'onClick' | 'onFocus' | 'onBlur'

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

type Params = ExtractParams<'/user/:id/post/:postId'>;
// 'id' | 'postId'

// 实际应用——API 路由类型安全
type API = `/api/${string}`;
// get endpoint: API = '/api/users'; // OK
// get endpoint: API = '/other'; // 报错
模板字面量类型在写 DSL 和类型安全的路由/事件系统时特别强大。但复杂的字符串模式可能变慢——不要写过于复杂的递归模板类型。

04. 声明合并与模块扩展

TypeScript 的声明合并让同名的 interface 或 namespace 自动合并在一起。这在扩展第三方库的类型时特别有用。 全局增强——用 declare global 给全局的 Window 或者 Express 的 Request 加自定义属性。 模块增强——用 declare module 给第三方库补充或覆盖类型定义。 .d.ts 文件——类型声明文件,只包含类型定义不包含运行时代码。放在项目的 types/ 目录或 @types 包中。 常用场景:Express 中间件给 req 加属性(如 req.user)、给 Window 加全局变量。
typescript
// 增强 Express Request
import 'express';

declare module 'express' {
  interface Request {
    user?: {
      id: number;
      role: string;
    };
  }
}

// 之后所有 req.user 都有类型

// 全局 Window
declare global {
  interface Window {
    __INITIAL_STATE__: Record<string, any>;
  }
}

// 给第三方库补类型
  
declare module 'some-lib' {
  export function newFunction(): void;
}

05. 严格模式与最佳实践

strict: true 开启后包含这些子检查: noImplicitAny——不允许推断类型为 any 的函数参数。 strictNullChecks——null 和 undefined 不能赋值给非空类型。 strictFunctionTypes——更严格的函数参数逆变检查。 strictBindCallApply——bind/call/apply 的参数类型检查。 strictPropertyInitialization——类的属性必须在构造函数里或声明时初始化。 noImplicitReturns——函数的所有分支必须都有返回值。 生产项目强烈建议 strict: true。虽然一开始报很多错,但这些报错的地方都是潜在的 bug 入口。改完以后代码质量提升巨大。
typescript
// strictNullChecks 保护的常见 bug
// 即使 strict 对 null 的处理,也需要合理配合
function findUser(id: number): User | undefined {
  // 可能找不到
  return users.find(u => u.id === id);
}

// strict 下这样写会报错
// get user = findUser(1);
// user.name;  // 报错!user 可能是 undefined

// 必须先检查
get user = findUser(1);
if (user) {
  console.log(user.name);  // 安全
}
strict: true 是一组检查的总开关。如果你不确定改哪个,直接 strict: true 全开。逐步关闭某个检查不如逐步解决所有报错。

知识测验

1/5正确 0

分布式条件类型是什么意思?

下一节

装饰器

下一节