模板字面量类型

字符串操作与模式匹配的类型表达
Uppercase
内置字符串变换
infer
模板内提取支持
Union
模板联合展开
1.4+
TS 版本引入

模板字面量类型基础

TypeScript 4.1 引入模板字面量类型,允许通过反引号语法定义字符串字面量类型的集合。与联合类型结合时,模板会展开所有可能的组合。

  • 字面量约束:精确约束字符串只能是特定格式(如 HTTP 方法、事件名称)
  • 字符串操作:支持 uppercase、lowercase、capitalize、uncapitalize 内置工具
  • 模式提取:通过 infer 在模板内提取字符串的特定部分
// 基础模板字面量类型
type HTTPMethod = 'GET' | 'POST' | 'PUT' | 'DELETE';

// 模板字面量与联合类型展开
type EventName = 'click' | 'focus' | 'blur';
type HandlerName = 'on-'.Capitalize<EventName>;
// HandlerName = 'on-Click' | 'on-Focus' | 'on-Blur'

// 实际应用:类型安全的 CSS 类名构造
type Size = 'sm' | 'md' | 'lg';
type Variant = 'primary' | 'secondary';
type ClassName = 'btn-'.Capitalize<Size> | 'btn-'.Capitalize<Variant>;
// ClassName = 'btn-Sm' | 'btn-Md' | 'btn-Lg' | 'btn-Primary' | 'btn-Secondary'

// 更复杂的场景:提取路径参数
type Route = '/users/:id' | '/products/:category/:id';

// 提取路径参数名称
type ExtractParams<T extends string> =
    T extends `${string}:${infer P}${string}` ? ExtractParams<DropFirst<T>> | P : never;

type UserParams = ExtractParams<'/users/:id'>;
// UserParams = 'id'

type ProductParams = ExtractParams<'/products/:category/:id'>;
// ProductParams = 'category' | 'id'
// 实际工程应用:类型安全的 API 路径构建
type ApiPaths = {
    '/users': { GET: UserListResponse; POST: CreateUserRequest };
    '/users/:id': { GET: UserResponse; PUT: UpdateUserRequest; DELETE: void };
    '/orders/:orderId/items': { GET: OrderItemsResponse };
};

type ExtractPathParams<T extends string> =
    T extends `${string}:${infer P}(${string}` extends `${infer _}:${infer Rest}`
        ? ExtractPathParams<`${string}:${Rest}`>
        : T extends `${string}:${infer P}`
            ? P
            : never;

type PathParams<T extends keyof ApiPaths> =
    ExtractPathParams<T>;

// 用例
type UserIdParam = PathParams<'/users/:id'>;
// UserIdParam = 'id'

IDE 自动补全

模板字面量类型在 IDE 中提供精确的智能提示。当定义 on${Event} 类型时,输入 on- 后会自动提示所有可能的 Event 组合。

大小写变换

内置的 Uppercase、Lowercase、Capitalize、Uncapitalize 可组合使用,实现命名规范的标准化转换。

模式匹配

模板字面量的 ${string} 占位符可与 infer 结合,实现复杂字符串结构的模式识别与提取。

类型守卫

运行时类型收窄与编译时推断

类型守卫的分类

类型守卫是 TypeScript 进行类型收窄的核心机制。它允许在条件分支中缩小联合类型的范围,使开发者能够在运行时安全地访问特定类型的属性和方法。

  • typeof 守卫:用于基础类型(string、number、boolean、bigint、symbol、undefined、function)
  • instanceof 守卫:用于类实例的类型检查
  • 自定义守卫:返回 is 谓词的函数,精确控制类型收窄逻辑
  • in 守卫:检查对象属性是否存在
// 自定义类型守卫
interface Cat { meow(): void; }
interface Dog { bark(): void; }

function isCat(animal: Cat | Dog): animal is Cat {
    return (animal as Cat).meow !== undefined;
}

function speak(animal: Cat | Dog) {
    if (isCat(animal)) {
        animal.meow();  // TypeScript 知道这是 Cat,meow() 可访问
    } else {
        animal.bark(); // TypeScript 知道这是 Dog,bark() 可访问
    }
}

// in 守卫
function processValue(value: { kind: 'success'; data: string } | { kind: 'error'; message: string }) {
    if ('data' in value) {
        console.log(value.data);  // success 分支
    } else {
        console.log(value.message); // error 分支
    }
}

// 可辨识联合(Discriminated Union)—— 最推荐的模式
type Result<T, E extends Error> =
    | { status: 'fulfilled'; value: T }
    | { status: 'rejected'; reason: E };

function handleResult<T, E extends Error>(result: Result<T, E>) {
    switch (result.status) {
        case 'fulfilled':
            return result.value;  // T 类型
        case 'rejected':
            throw result.reason;    // E 类型
    }
}

typeof 的局限

typeof 仅能识别 JS 原始类型和 function,无法区分具体对象类型或类层次。当需要区分两个不同的 interface 时,typeof 无能为力,需要自定义守卫或可辨识联合。

never 收窄

类型守卫配合 exhaustive check(穷尽性检查)可以确保所有联合成员都被处理。如果遗漏分支,TypeScript 会将剩余类型收窄为 never,触发编译错误。

工程实践

可辨识联合(Discriminated Union)是 TypeScript 中最可靠的结构化类型检查模式。为每个联合成员添加唯一的 discriminant 字段(如 kind/status/type),使类型收窄在任意条件下都可预测。

条件类型分发

联合类型展开与联合计算
Distribute
联合类型分发规则
Outer
外层条件分发
[T] extends
抑制分发技巧
Union
分发结果并集

条件类型的分发机制

当条件类型(T extends U ? X : Y)中的 T 是联合类型时,TypeScript 会将条件分发到联合的每个成员,分别计算后将结果合并为新的联合类型。这是对条件类型最强大也最容易被误解的特性。

  • 分发触发条件:T 必须是裸类型(未包裹在任何泛型中)。[T] extends 语法可抑制分发
  • 分发方向:联合的每个元素分别代入条件,计算结果后合并
  • 空联合:never 作为泛型时,条件类型直接返回 never(因为 never 无任何可分配类型)
// 分发示例:联合类型的展开
type ToArray<T> = T extends any ? T[] : never;

type StrOrNumArr = ToArray<'a' | number>;
// 展开过程: ToArray<'a'> | ToArray = 'a'[] | number[]
// 最终结果: ('a'[] | number[]) —— 这是联合,不是交叉

// 抑制分发:[T] extends 而不是 T extends
type IsNever<T> = [T] extends [never] ? true : false;

type CheckString = IsNever<'string'>;  // false —— 不分发
type CheckNever = IsNever<never>;     // true  —— never 特殊处理

// 实际应用:类型过滤(Exclude / Extract 原理)
type MyExclude<T, U> = T extends U ? never : T;

type Filtered = MyExclude<'a' | 'b' | 'c', 'b'>;
// 展开: ('a' extends 'b' ? never : 'a') | ('b' extends 'b' ? never : 'b') | ('c' extends 'b' ? never : 'c')
// 结果: 'a' | never | 'c' = 'a' | 'c'

// 嵌套分发:更复杂的场景
type Flatten<T> = T extends Array<infer U> ? U : T;

type Nested = (string[] | number[])[];
type FlatResult = Flatten<Nested>;
// 分发后: Flatten<string[]> | Flatten<number[]> = string | number

分发边界

条件类型在裸泛型时触发分发。如果 T 被 Array<T> 或 [T] 包裹,分发会被抑制。这在实现 IsAny、IsNever 等工具类型时至关重要。

分发与交叉

联合类型的分发会产生联合结果;交叉类型(&)则不会分发,结果是各分支的交叉。理解这个差异是高级类型编程的基础。

分发与 any

any 与任何类型 extends 时,由于 any 的特殊性,分发结果为所有可能的联合。慎用 any 作为条件类型的输入。

infer 推导

模式匹配与类型提取机制

infer 的工作机制

infer 只能在条件类型的 extends 子句中使用,用于声明一个局部类型变量,让 TypeScript 在模式匹配时自动推断并捕获该位置的类型。它是实现高级工具类型(如 ReturnType、Parameters)的基础机制。

  • 只能在 extends 子句中使用:infer 是模式匹配的一部分,不能独立存在
  • 延迟推断:当条件成立时,infer 声明的类型变量才会被具体化
  • 多 infer 联合:可以在同一个模式中使用多个 infer 声明
// 经典工具类型:ReturnType 实现
type ReturnType<T extends (...args: any) => any> =
    T extends (...args: any) => infer R ? R : never;

type Fn = (name: string, age: number) => Promise<{ id: string }>;
type FnReturn = ReturnType<Fn>;
// FnReturn = Promise<{ id: string }>

// Parameters:提取函数参数类型
type Parameters<T extends (...args: any) => any> =
    T extends (...args: infer P) => any ? P : never;

// 递归 infer:提取嵌套类型
type DeepAwaited<T> = T extends Promise<infer U>
    ? DeepAwaited<U>  // 递归处理嵌套 Promise
    : T;

type DeepResult = DeepAwaited<Promise<Promise<Promise<string>>>>;
// DeepResult = string

// 提取数组元素类型(递归)
type DeepExtract<T> = T extends Array<infer U> ? DeepExtract<U> : T;

type Extracted = DeepExtract<string[][][]>;
// Extracted = string

// 构造函数类型推断
type ConstructorParams<T extends new (...args: any) => any> =
    T extends new (...args: infer P) => any ? P : never;

class User {
    constructor(name: string, age: number) {}
}

type UserCtorParams = ConstructorParams<typeof User>;
// UserCtorParams = [name: string, age: number]

infer 在模板字面量中的使用

模板字面量类型支持 infer,可在字符串的特定位置提取子串。结合 Uppercase 等内置工具,可实现类型安全的字符串解析。

  • ${infer Prefix}static${infer _}:提取 static 前面的部分
  • get${infer Name}:提取 get 后面的名称字面量

infer 与分布式条件类型

当 infer 遇到联合类型时,会对每个联合成员分别推断,然后将结果联合起来。如果希望抑制分布式,应使用 [T] extends 而非裸 T。

核心模式

infer 的本质是模式匹配中的"捕获变量"。当 extends 左侧的模式与右侧类型匹配时,infer 声明的位置会捕获对应的子类型。这是 TypeScript 类型系统实现递归计算的核心机制。

映射类型修饰符

readonly、可选与可变性的控制
-readonly
移除只读
-?
移除可选
+readonly
添加只读
+?
添加可选

映射类型(Mapped Types)

映射类型是 TypeScript 类型系统中最强大的工具之一,它通过 { [K in keyof T]: ... } 语法遍历一个类型的所有键,并逐个进行转换。加上修饰符前缀可以精细控制每个属性的可读性和可选性。

  • readonly:控制属性是否可写。-readonly 移除只读,+readonly 添加只读
  • ?:控制属性是否可选。-? 移除可选(变为必需),+? 添加可选
  • as 重新映射(TS 4.1+):重新映射键名,实现键的过滤与变换
// 基础映射类型:给所有属性添加只读
type Readonly<T> = {
    readonly [K in keyof T]: T[K];
};

// 移除只读(-readonly)
type Mutable<T> = {
    -readonly [K in keyof T]: T[K];
};

// 移除可选(-?)—— 所有属性变为必需
type Required<T> = {
    [K in keyof T -?]: T[K];
};

// 添加可选(+?)—— 所有属性变为可选
type Partial<T> = {
    [K in keyof T +?]: T[K];
};

// 键名重新映射(as 子句)
type Getters<T> = {
    [K in keyof T as `get${Capitalize<string & K>}`]: () => T[K];
};

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

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

// 键过滤:仅保留 string 类型的属性
type StringOnly<T> = {
    [K in keyof T as T[K] extends string ? K : never]: T[K];
};

// 条件映射:仅处理特定类型的属性
type DeepReadonly<T> = {
    readonly [K in keyof T]: T[K] extends object ? DeepReadonly<T[K]> : T[K];
};

同态映射

映射类型作用于某个类型时,如果映射表达式直接引用原类型属性(T[K]),则该映射是同态的——它会保留原始类型的结构特性(如只读、可选)。

异态映射

不直接引用 T[K] 的映射是异态的,如 { [K in keyof T]: SomeOtherType }。异态映射会打破原类型的 readonly 和可选特性。

键名重映射

TS 4.1 的 as 子句允许重新映射键名,可结合条件类型实现键的过滤(never)和变换(模板字面量)。

高级模式:类型级计算

映射类型配合条件类型和 infer,可以实现类型级的深度计算。如 DeepPartial(深层可选)、DeepMutable(深层可变)、UnionToIntersection(联合转交叉)等。这些工具类型是类型安全框架的核心支柱。