高级 TypeScript 指南:泛型、条件类型、模板字面量、装饰器与类型级编程
深入探讨 TypeScript 最强大的类型系统特性,从高级泛型到类型级编程模式。
- ✓ 泛型约束和默认类型使可复用 API 既灵活又类型安全。
- ✓ 带 infer 的条件类型实现强大的类型提取和转换。
- ✓ 映射类型配合键重映射和模板字面量自动创建派生类型。
- ✓ 可辨识联合加穷举检查消除整类运行时错误。
- ✓ 品牌类型在结构类型系统中提供名义类型化。
- ✓ satisfies 运算符在不拓宽类型的情况下验证类型,保留字面量推断。
为什么高级 TypeScript 很重要
基本的 TypeScript 注解(string、number、boolean)只是冰山一角。类型系统包含泛型、条件类型、映射类型、模板字面量类型和递归类型,让你在编译时表达复杂的领域规则。
掌握这些特性可以减少运行时错误、改善 API 设计,并实现类型安全的事件系统、经过验证的 API 响应和完全类型化的 ORM 查询等模式。本指南通过实际示例逐一讲解每个特性。
1. 高级泛型
泛型是可复用类型化代码的基础。除了基本用法外,TypeScript 还支持泛型约束、默认类型参数和变型注解,提供细粒度控制。
Generic Constraints
泛型约束使用 extends 关键字限制泛型参数可以接受的类型。这确保泛型仅与具有所需结构的类型一起工作。
// Generic constraint: T must have a length property
function longest<T extends { length: number }>(a: T, b: T): T {
return a.length >= b.length ? a : b;
}
longest("hello", "hi"); // OK: string has length
longest([1, 2, 3], [1]); // OK: array has length
// longest(10, 20); // Error: number has no length
// Constraint with keyof
function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] {
return obj[key];
}
const user = { name: "Alice", age: 30 };
getProperty(user, "name"); // type: string
getProperty(user, "age"); // type: number
// getProperty(user, "email"); // Error: "email" not in keyof UserDefault Type Parameters
默认类型参数在调用者未指定时提供回退类型。这类似于默认函数参数,减少调用端的样板代码。
// Default type parameter
interface ApiResponse<TData = unknown, TError = Error> {
data: TData | null;
error: TError | null;
status: number;
}
// Uses defaults: ApiResponse<unknown, Error>
const res1: ApiResponse = { data: null, error: null, status: 200 };
// Override only TData
const res2: ApiResponse<User[]> = {
data: [{ id: 1, name: "Alice" }],
error: null,
status: 200,
};Variance Annotations
变型注解(in 和 out 关键字,TypeScript 4.7 新增)显式标记泛型参数是协变(out)、逆变(in)还是不变的。这提高了类型检查的正确性和性能。
// Variance annotations (TypeScript 4.7+)
interface Producer<out T> {
produce(): T; // T is in output (covariant) position
}
interface Consumer<in T> {
consume(value: T): void; // T is in input (contravariant) position
}
interface Transform<in T, out U> {
transform(input: T): U; // T contravariant, U covariant
}2. 条件类型与 Infer
条件类型遵循 T extends U ? X : Y 模式。它们实现类型级分支,根据条件是否满足选择不同类型。
// Basic conditional type
type IsString<T> = T extends string ? true : false;
type A = IsString<"hello">; // true
type B = IsString<42>; // false
type C = IsString<string>; // true
// Nested conditional types
type TypeName<T> =
T extends string ? "string" :
T extends number ? "number" :
T extends boolean ? "boolean" :
T extends undefined ? "undefined" :
T extends Function ? "function" :
"object";The infer Keyword
条件类型中的 infer 关键字引入一个类型变量,TypeScript 从被检查的类型中推断它。这就是 ReturnType 和 Parameters 等工具类型的工作原理。
// Extract return type using infer
type MyReturnType<T> = T extends (...args: any[]) => infer R ? R : never;
type R1 = MyReturnType<() => string>; // string
type R2 = MyReturnType<(x: number) => boolean>; // boolean
// Extract Promise inner type
type UnwrapPromise<T> = T extends Promise<infer U> ? U : T;
type P1 = UnwrapPromise<Promise<string>>; // string
type P2 = UnwrapPromise<Promise<number[]>>; // number[]
type P3 = UnwrapPromise<string>; // string
// Extract array element type
type ElementOf<T> = T extends (infer E)[] ? E : never;
type E1 = ElementOf<string[]>; // string
type E2 = ElementOf<[1, 2, 3]>; // 1 | 2 | 3Distributive Conditional Types
分布式条件类型自动分布在联合类型上。如果 T 是 A | B,则 T extends U ? X : Y 变为 (A extends U ? X : Y) | (B extends U ? X : Y)。用 [T] 包裹以阻止分布。
// Distributive: T distributes over union
type ToArray<T> = T extends any ? T[] : never;
type D1 = ToArray<string | number>;
// Result: string[] | number[] (distributed)
// Non-distributive: wrap in tuple
type ToArrayND<T> = [T] extends [any] ? T[] : never;
type D2 = ToArrayND<string | number>;
// Result: (string | number)[] (not distributed)3. 映射类型与键重映射
映射类型遍历类型的键来创建新类型。基本形式是 { [K in keyof T]: NewType }。结合条件类型和模板字面量,映射类型变得极其强大。
// Basic mapped type: make all properties optional
type MyPartial<T> = {
[K in keyof T]?: T[K];
};
// Make all properties readonly
type MyReadonly<T> = {
readonly [K in keyof T]: T[K];
};
// Mapped type with conditional value
type NullableProps<T> = {
[K in keyof T]: T[K] | null;
};Key Remapping (as clause)
键重映射(as 子句,TypeScript 4.1+)允许在映射过程中转换键。你可以使用模板字面量类型重命名、过滤或生成新键。
// Key remapping with as (TypeScript 4.1+)
type Getters<T> = {
[K in keyof T as `get${Capitalize<string & K>}`]: () => T[K];
};
interface Person {
name: string;
age: number;
}
type PersonGetters = Getters<Person>;
// { getName: () => string; getAge: () => number }
// Filter keys: remove methods
type DataOnly<T> = {
[K in keyof T as T[K] extends Function ? never : K]: T[K];
};Template Literal Types
模板字面量类型(TypeScript 4.1 引入)在类型级别实现字符串操作。结合映射类型,它们可以生成类型化的事件处理器、API 端点或 CSS 属性类型。
// Template literal types
type EventName<T extends string> = `on${Capitalize<T>}`;
type ClickEvent = EventName<"click">; // "onClick"
type FocusEvent = EventName<"focus">; // "onFocus"
// Combining with union types
type Color = "red" | "blue" | "green";
type Size = "small" | "medium" | "large";
type ColorSize = `${Color}-${Size}`;
// "red-small" | "red-medium" | "red-large"
// | "blue-small" | "blue-medium" | ...
// Built-in string manipulation types
type U = Uppercase<"hello">; // "HELLO"
type L = Lowercase<"HELLO">; // "hello"
type Cap = Capitalize<"hello">; // "Hello"
type Unc = Uncapitalize<"Hello">; // "hello"4. 工具类型深入剖析
TypeScript 内置的工具类型使用泛型、条件类型和映射类型实现。理解其内部原理有助于编写自己的工具类型。
| Utility Type | Implementation | Purpose |
|---|---|---|
Partial<T> | { [K in keyof T]?: T[K] } | All properties optional |
Required<T> | { [K in keyof T]-?: T[K] } | All properties required |
Pick<T, K> | { [P in K]: T[P] } | Select specific keys |
Omit<T, K> | Pick<T, Exclude<keyof T, K>> | Remove specific keys |
Record<K, V> | { [P in K]: V } | Object with key type K, value type V |
ReturnType<T> | T extends (...) => infer R ? R : any | Extract function return type |
Parameters<T> | T extends (...args: infer P) => any ? P : never | Extract parameter types as tuple |
Awaited<T> | Recursive Promise unwrap | Unwrap nested Promises |
Exclude<T, U> 从联合中移除类型。Extract<T, U> 保留匹配的类型。ReturnType<T> 提取函数返回类型。Parameters<T> 提取参数类型为元组。NonNullable<T> 移除 null 和 undefined。
// Practical utility type compositions
type UpdatePayload<T> = Partial<Omit<T, "id" | "createdAt">>;
interface User {
id: string;
name: string;
email: string;
createdAt: Date;
}
type UserUpdate = UpdatePayload<User>;
// { name?: string; email?: string }
// Custom utility: make specific keys required
type RequireKeys<T, K extends keyof T> =
Omit<T, K> & Required<Pick<T, K>>;
type UserWithEmail = RequireKeys<Partial<User>, "email">;
// { id?: string; name?: string; email: string; createdAt?: Date }Awaited<T>(TypeScript 4.5+)递归解包 Promise 类型。NoInfer<T>(TypeScript 5.4+)阻止特定位置的推断。这些较新的工具类型解决常见痛点。
5. 可辨识联合与穷举检查
可辨识联合将联合类型与共享的字面量属性(辨识符)结合。TypeScript 根据此辨识符收窄类型,实现对变体特定属性的安全访问。
// Discriminated union with "type" discriminant
type Shape =
| { type: "circle"; radius: number }
| { type: "rectangle"; width: number; height: number }
| { type: "triangle"; base: number; height: number };
function area(shape: Shape): number {
switch (shape.type) {
case "circle":
return Math.PI * shape.radius ** 2;
case "rectangle":
return shape.width * shape.height;
case "triangle":
return (shape.base * shape.height) / 2;
}
}Exhaustive Checking with never
穷举检查确保处理了每个变体。never 类型是关键:如果 switch 语句遗漏了一个 case,剩余类型不能赋值给 never,导致编译错误。
// Exhaustive check helper
function assertNever(x: never): never {
throw new Error("Unexpected value: " + x);
}
function getShapeColor(shape: Shape): string {
switch (shape.type) {
case "circle": return "red";
case "rectangle": return "blue";
case "triangle": return "green";
default:
// If a new variant is added to Shape but not handled,
// TypeScript will error here at compile time
return assertNever(shape);
}
}此模式非常适合状态机、Redux action、不同形状的 API 响应、编译器 AST 节点以及任何具有多个变体共享公共接口的场景。
6. 品牌类型与名义类型
TypeScript 使用结构类型:两个具有相同结构的类型是兼容的。品牌类型添加一个幻影属性来创建名义类型,即使运行时值相同也结构不兼容。
// Branded type pattern
type Brand<T, B extends string> = T & { readonly __brand: B };
type UserId = Brand<string, "UserId">;
type OrderId = Brand<string, "OrderId">;
type Email = Brand<string, "Email">;
// Constructor functions with validation
function createUserId(id: string): UserId {
if (!id.startsWith("usr_")) {
throw new Error("Invalid user ID format");
}
return id as UserId;
}
function createEmail(value: string): Email {
if (!value.includes("@")) {
throw new Error("Invalid email");
}
return value as Email;
}
function getUser(id: UserId): void { /* ... */ }
function getOrder(id: OrderId): void { /* ... */ }
const userId = createUserId("usr_123");
const orderId = "ord_456" as OrderId;
getUser(userId); // OK
// getUser(orderId); // Error: OrderId not assignable to UserId这对防止意外误用至关重要:在需要 OrderId 的地方传递 UserId,或混淆已验证和未验证的字符串。品牌仅存在于类型级别,零运行时开销。
7. 声明合并与模块增强
声明合并自动组合同名的多个声明。接口合并其成员。命名空间与类、函数和枚举合并。这就是 TypeScript 扩展内置类型的方式。
// Interface merging
interface Window {
analytics: {
track(event: string, data?: object): void;
};
}
// Now window.analytics.track() is typed
window.analytics.track("page_view", { path: "/" });Module Augmentation
模块增强允许在不修改源码的情况下扩展第三方库类型。使用 declare module 为 Express Request 添加属性、扩展 Window 或修补库类型。
// Augment Express Request type
declare module "express" {
interface Request {
user?: {
id: string;
role: "admin" | "user";
};
requestId: string;
}
}
// Augment a CSS module
declare module "*.module.css" {
const classes: Record<string, string>;
export default classes;
}
// Augment environment variables
declare namespace NodeJS {
interface ProcessEnv {
DATABASE_URL: string;
API_KEY: string;
NODE_ENV: "development" | "production" | "test";
}
}8. 装饰器(Stage 3 / TC39)
Stage 3 装饰器(TypeScript 5.0+)是用于在定义时修改类、方法、属性和访问器的标准提案。它们替代了需要 --experimentalDecorators 标志的旧实验性装饰器。
装饰器是接收目标值和上下文对象的函数。它可以返回替代值或 undefined。类装饰器接收类构造函数。方法装饰器接收方法函数。
// Stage 3 decorator: method logging
function log<T extends (...args: any[]) => any>(
target: T,
context: ClassMethodDecoratorContext
): T {
const methodName = String(context.name);
return function (this: any, ...args: any[]) {
console.log(`Calling ${methodName} with`, args);
const result = target.apply(this, args);
console.log(`${methodName} returned`, result);
return result;
} as T;
}
// Class decorator: sealed
function sealed<T extends new (...args: any[]) => any>(
target: T,
_context: ClassDecoratorContext
): T {
Object.seal(target);
Object.seal(target.prototype);
return target;
}// Using decorators
@sealed
class UserService {
@log
findById(id: string): User | null {
// ... database lookup
return null;
}
@log
create(data: CreateUserDto): User {
// ... insert logic
return { id: "1", ...data } as User;
}
}常见用例包括日志记录、验证、记忆化、依赖注入、访问控制和序列化元数据。装饰器在堆叠时自然组合。
9. satisfies 运算符
satisfies 运算符(TypeScript 4.9+)验证表达式匹配类型而不改变推断类型。与类型注解(: Type)不同,satisfies 保留字面量类型和特定联合成员。
// Without satisfies: type annotation widens
type ColorMap = Record<string, [number, number, number] | string>;
const colorsAnnotated: ColorMap = {
red: [255, 0, 0],
green: "#00ff00",
blue: [0, 0, 255],
};
// colorsAnnotated.red is [number, number, number] | string
// Cannot call .toUpperCase() even on green!
// With satisfies: preserves literal inference
const colors = {
red: [255, 0, 0],
green: "#00ff00",
blue: [0, 0, 255],
} satisfies ColorMap;
colors.green.toUpperCase(); // OK! TypeScript knows it is string
colors.red[0]; // OK! TypeScript knows it is number这解决了长期存在的矛盾:你需要类型检查以确保正确性,但不想丢失特定的推断类型。使用 satisfies,两者兼得。
// Route config with satisfies
type Route = { path: string; component: string; auth?: boolean };
const routes = {
home: { path: "/", component: "HomePage" },
profile: { path: "/profile", component: "ProfilePage", auth: true },
login: { path: "/login", component: "LoginPage" },
} satisfies Record<string, Route>;
// routes.home.path is "/", not just string!
// routes.profile.auth is true, not just boolean | undefined!10. Const 断言与只读模式
as const 断言将值转换为最具体的字面量类型。数组变为只读元组,对象获得只读属性和字面量类型,字符串值被收窄为其精确字面量。
// as const narrows to literal types
const config = {
endpoint: "https://api.example.com",
retries: 3,
methods: ["GET", "POST"],
} as const;
// typeof config:
// {
// readonly endpoint: "https://api.example.com";
// readonly retries: 3;
// readonly methods: readonly ["GET", "POST"];
// }
// Derive union type from const array
const HTTP_METHODS = ["GET", "POST", "PUT", "DELETE"] as const;
type HttpMethod = (typeof HTTP_METHODS)[number];
// "GET" | "POST" | "PUT" | "DELETE"将 as const 与泛型函数结合可实现强大的模式,如类型安全的构建器、路由定义和配置对象,其中精确的形状在类型级别得以保留。
// Type-safe route builder with as const
function defineRoutes<
T extends Record<string, { path: string }>
>(routes: T): T {
return routes;
}
const appRoutes = defineRoutes({
home: { path: "/" },
about: { path: "/about" },
blog: { path: "/blog" },
} as const);
// appRoutes.home.path is exactly "/", not string11. 类型收窄模式
类型收窄是 TypeScript 在代码块中将宽泛类型细化为更具体类型的方式。除了 typeof 和 instanceof,TypeScript 还支持自定义类型守卫、断言函数和控制流收窄。
User-Defined Type Guards
用户定义的类型守卫(is 关键字)返回布尔值并收窄参数类型。断言函数(asserts 关键字)在条件失败时抛出异常,并在调用后收窄类型。
// Type guard with is keyword
interface Fish { swim(): void }
interface Bird { fly(): void }
function isFish(pet: Fish | Bird): pet is Fish {
return (pet as Fish).swim !== undefined;
}
function move(pet: Fish | Bird) {
if (isFish(pet)) {
pet.swim(); // TypeScript knows pet is Fish
} else {
pet.fly(); // TypeScript knows pet is Bird
}
}
// Assertion function (asserts keyword)
function assertDefined<T>(
value: T | null | undefined,
message?: string
): asserts value is T {
if (value == null) {
throw new Error(message ?? "Value is null or undefined");
}
}
function processUser(user: User | null) {
assertDefined(user, "User not found");
// After this line, user is narrowed to User
console.log(user.name);
}Advanced Narrowing Patterns
in 运算符、等值检查、真值检查和赋值收窄都能细化类型。TypeScript 通过 if/else 分支、switch 语句和短路求值跟踪收窄。
// in operator narrowing
type Admin = { role: "admin"; permissions: string[] };
type Guest = { role: "guest"; visitCount: number };
function greet(user: Admin | Guest) {
if ("permissions" in user) {
// user is Admin
console.log("Admin with", user.permissions.length, "perms");
} else {
// user is Guest
console.log("Guest visit #", user.visitCount);
}
}12. 协变与逆变
协变意味着子类型可以替代超类型(Cat[] 可赋值给 Animal[])。逆变意味着相反方向:超类型可以替代子类型。这对函数参数和返回类型很重要。
// Covariance: subtype in output position
class Animal { name = "animal"; }
class Dog extends Animal { breed = "labrador"; }
// Return type is covariant:
// () => Dog is assignable to () => Animal
type AnimalFactory = () => Animal;
const dogFactory: AnimalFactory = (): Dog => new Dog();
// Contravariance: supertype in input position
// (with --strictFunctionTypes)
type AnimalHandler = (animal: Animal) => void;
type DogHandler = (dog: Dog) => void;
// AnimalHandler is NOT assignable to DogHandler
// (contravariant: parameter types go in opposite direction)
// const handler: DogHandler = (a: Animal) => {}; // Error函数参数是逆变的(除非关闭 --strictFunctionTypes)。返回类型是协变的。理解变型可以防止在传递回调、比较函数类型或使用泛型容器时出现微妙的 bug。
13. 递归类型
递归类型在其定义中引用自身。它们对于建模树结构、嵌套 JSON、深层嵌套配置、链表以及任何无限深度的数据至关重要。
// Recursive type: JSON value
type JsonValue =
| string
| number
| boolean
| null
| JsonValue[]
| { [key: string]: JsonValue };
// Recursive type: tree structure
type TreeNode<T> = {
value: T;
children: TreeNode<T>[];
};
const tree: TreeNode<string> = {
value: "root",
children: [
{ value: "child1", children: [] },
{
value: "child2",
children: [
{ value: "grandchild", children: [] }
],
},
],
};TypeScript 支持递归类型别名(3.7 起)和递归条件类型(4.1 起)。这些使得 DeepPartial、DeepReadonly、深层路径提取和 JSON 类型验证等强大工具类型成为可能。
// DeepPartial: recursively make all properties optional
type DeepPartial<T> = {
[K in keyof T]?: T[K] extends object
? DeepPartial<T[K]>
: T[K];
};
// DeepReadonly: recursively make all properties readonly
type DeepReadonly<T> = {
readonly [K in keyof T]: T[K] extends object
? DeepReadonly<T[K]>
: T[K];
};
// Recursive path extraction
type Path<T, Prefix extends string = ""> = {
[K in keyof T & string]: T[K] extends object
? Path<T[K], `${Prefix}${K}.`>
: `${Prefix}${K}`;
}[keyof T & string];
interface Config {
db: { host: string; port: number };
cache: { ttl: number };
}
type ConfigPaths = Path<Config>;
// "db.host" | "db.port" | "cache.ttl"总结
TypeScript 的高级类型特性构成了完整的类型级编程语言。通过掌握泛型、条件类型、映射类型、模板字面量以及本指南中的模式,你可以构建自文档化、不可能误用、在编译时而非运行时捕获错误的 API。
从与当前代码库最相关的模式开始。可辨识联合和穷举检查提供即时价值。品牌类型和自定义工具类型随领域模型的增长变得必不可少。递归类型和模板字面量类型等高级模式解锁了以前在静态类型语言中不可能实现的能力。
FAQ
何时应该使用泛型而非联合类型?
当需要在函数或类中保留并传播特定类型时使用泛型。当有已知的、有限的可能类型集时使用联合类型。泛型维护输入和输出类型之间的关系;联合只是在一个位置允许多种类型。
TypeScript 中 type 和 interface 的区别是什么?
接口支持声明合并,可以用 extends 扩展。类型支持联合、交叉、条件和映射类型。对于对象形状,两者都可以;公共 API 优先用 interface(可合并),复杂类型操作用 type。
带 infer 的条件类型如何工作?
条件类型使用 T extends U ? X : Y 模式。extends 子句中的 infer 关键字引入一个类型变量,TypeScript 从 T 中推断它。例如,T extends Promise<infer R> ? R : T 从 Promise 中提取解析后的类型。
什么是品牌类型,何时应该使用?
品牌类型添加一个幻影属性(仅在编译时存在)来在 TypeScript 的结构类型系统中创建类似名义的类型。用它们防止混淆语义不同的值,如 UserId 与 OrderId,或已验证与未验证的字符串。
satisfies 运算符与类型注解有什么不同?
类型注解(const x: Type)将推断类型拓宽为匹配注解。satisfies 运算符(const x = value satisfies Type)检查值是否匹配类型但保留原始的窄/字面量推断。你得到类型检查而不失去具体性。
Stage 3 装饰器是什么,与实验性装饰器有何不同?
Stage 3 装饰器(TypeScript 5.0+)遵循 TC39 标准提案,不需要 --experimentalDecorators。它们接收值和上下文对象而非 target/key/descriptor。它们与 Angular 或 MobX 等旧库使用的遗留装饰器不向后兼容。
如何在 switch 语句中进行穷举检查?
处理所有已知 case 后,添加一个 default case,将辨识符赋值给类型为 never 的变量。如果遗漏了一个 case,TypeScript 会报错,因为剩余类型不能赋值给 never。这确保在编译时处理所有变体。
递归类型有什么用途?
递归类型建模自引用数据,如树、嵌套 JSON、链表和深层嵌套配置。它们使 DeepPartial、DeepReadonly 和深层路径提取等工具类型能够操作任意嵌套的结构。