TypeScript 进阶
前言
TypeScript 的类型系统是图灵完备的,这意味着你可以在类型层面实现几乎任何逻辑。本文档深入讲解 TypeScript 的高级类型特性,从装饰器到类型体操,帮助你掌握类型编程的核心能力。
一、装饰器
装饰器是一种特殊的声明,可以附加到类、方法、属性或参数上。它们使用 @expression 语法,其中 expression 是一个在运行时被调用的函数。
1.1 启用装饰器
在 tsconfig.json 中启用:
{
"compilerOptions": {
"experimentalDecorators": true,
"emitDecoratorMetadata": true
}
}1.2 类装饰器
类装饰器在类声明之前被调用,用于观察、修改或替换类定义。
function sealed<TFunction extends { new (...args: any[]): {} }>(
constructor: TFunction
) {
Object.freeze(constructor);
Object.freeze(constructor.prototype);
}
@sealed
class BugReport {
title: string;
constructor(title: string) {
this.title = title;
}
}1.3 方法装饰器
方法装饰器接收三个参数:类的原型、方法名称和属性描述符。常用于日志、拦截或修改方法行为。
function log(
target: any,
propertyKey: string,
descriptor: PropertyDescriptor
) {
const originalMethod = descriptor.value;
descriptor.value = function (...args: any[]) {
console.log(`调用 ${propertyKey}:`, args);
const result = originalMethod.apply(this, args);
console.log(`返回值:`, result);
return result;
};
}
class Calculator {
@log
add(a: number, b: number): number {
return a + b;
}
}1.4 属性装饰器
属性装饰器接收两个参数:类的原型和属性名称。它没有属性描述符,因此主要用于观察或元数据添加。
function defaultValue(value: string) {
return function (target: any, propertyKey: string) {
target[propertyKey] = value;
};
}
class User {
@defaultValue('未知用户')
name: string;
}1.5 参数装饰器
参数装饰器接收三个参数:类的原型、方法名称和参数索引。通常与 reflect-metadata 库结合使用。
import 'reflect-metadata';
function validate(target: any, propertyKey: string, parameterIndex: number) {
const existingParams: number[] =
Reflect.getOwnMetadata('validate_params', target, propertyKey) || [];
existingParams.push(parameterIndex);
Reflect.defineMetadata('validate_params', existingParams, target, propertyKey);
}
class UserService {
createUser(@validate name: string, @validate age: number) {
console.log(`创建用户: ${name}, ${age}`);
}
}1.6 装饰器工厂
装饰器工厂是一个返回装饰器函数的函数,允许通过参数定制装饰器行为。
function logWithPrefix(prefix: string) {
return function (
target: any,
propertyKey: string,
descriptor: PropertyDescriptor
) {
const original = descriptor.value;
descriptor.value = function (...args: any[]) {
console.log(`[${prefix}] 调用 ${propertyKey}`);
return original.apply(this, args);
};
};
}
class Logger {
@logWithPrefix('DEBUG')
info(message: string) {
console.log(message);
}
}二、条件类型
条件类型根据条件选择不同的类型,语法类似于三元运算符:T extends U ? X : Y。
2.1 基础条件类型
type IsNumber<T> = T extends number ? 'yes' : 'no';
type A = IsNumber<42>; // 'yes'
type B = IsNumber<'hi'>; // 'no'2.2 分布式条件类型
当条件类型作用于泛型且传入联合类型时,条件类型会分布式地应用于联合类型的每个成员。
type ToArray<T> = T extends unknown ? T[] : never;
type Result = ToArray<string | number>;
// string[] | number[] —— 而不是 (string | number)[]要关闭分布式行为,使用方括号包裹泛型参数:
type ToArrayNonDist<T> = [T] extends [unknown] ? T[] : never;
type Result2 = ToArrayNonDist<string | number>;
// (string | number)[]2.3 实用条件类型
// 提取函数返回类型
type ReturnOf<T> = T extends (...args: any[]) => infer R ? R : never;
// 排除 null 和 undefined
type NonNullable_<T> = T extends null | undefined ? never : T;
type T1 = ReturnOf<() => string>; // string
type T2 = NonNullable_<string | null>; // string三、映射类型
映射类型基于旧类型的属性创建新类型,是类型编程的核心工具之一。
3.1 基础映射类型
type Readonly_<T> = {
readonly [P in keyof T]: T[P];
};
type Partial_<T> = {
[P in keyof T]?: T[P];
};
interface Person {
name: string;
age: number;
}
type ReadonlyPerson = Readonly_<Person>;
type PartialPerson = Partial_<Person>;3.2 使用 as 重映射键
TypeScript 4.1 引入了键重映射,使用 as 子句可以修改属性名。
type Getters<T> = {
[P in keyof T as `get${Capitalize<string & P>}`]: () => T[P];
};
interface Person {
name: string;
age: number;
}
type GetterPerson = Getters<Person>;
// { getName: () => string; getAge: () => number; }3.3 过滤属性
通过 as 子句配合条件类型可以过滤属性:
type FilterStringProperties<T> = {
[P in keyof T as T[P] extends string ? P : never]: T[P];
};
interface Example {
name: string;
age: number;
email: string;
}
type OnlyStrings = FilterStringProperties<Example>;
// { name: string; email: string; }3.4 属性转换
// 将所有属性值改为 Promise 版本
type Promisify<T> = {
[P in keyof T]: Promise<T[P]>;
};
// 添加前缀并保留原始值
type WithPrefix<T, Prefix extends string> = {
[P in keyof T as `${Prefix}${Capitalize<string & P>}`]: T[P];
};四、类型体操实战
4.1 DeepPartial
递归地将所有属性变为可选:
type DeepPartial<T> = T extends object
? { [P in keyof T]?: DeepPartial<T[P]> }
: T;
interface Config {
server: {
host: string;
port: number;
};
database: {
url: string;
credentials: {
user: string;
password: string;
};
};
}
type PartialConfig = DeepPartial<Config>;
// 所有层级的属性都变为可选4.2 DeepReadonly
递归地将所有属性变为只读:
type DeepReadonly<T> = T extends object
? { readonly [P in keyof T]: DeepReadonly<T[P]> }
: T;
type ReadonlyConfig = DeepReadonly<Config>;
// 所有层级的属性都变为 readonly4.3 TupleToUnion
将元组类型转换为联合类型:
type TupleToUnion<T extends readonly any[]> = T[number];
type Colors = ['red', 'green', 'blue'];
type ColorUnion = TupleToUnion<Colors>;
// 'red' | 'green' | 'blue'
// 高级版本:支持可变参数
type TupleToUnionAdv<T extends any[]> =
T extends [infer First, ...infer Rest]
? First | TupleToUnionAdv<Rest>
: never;4.4 字符串模板类型
// 提取路由参数
type ExtractRouteParams<Route extends string> =
Route extends `${infer _Start}:${infer Param}/${infer Rest}`
? Param | ExtractRouteParams<Rest>
: Route extends `${infer _Start}:${infer Param}`
? Param
: never;
type Params = ExtractRouteParams<'/user/:id/post/:postId'>;
// 'id' | 'postId'
// 连接字符串数组
type Join<T extends string[], Separator extends string> =
T extends [infer First extends string, ...infer Rest extends string[]]
? Rest extends []
? First
: `${First}${Separator}${Join<Rest, Separator>}`
: '';4.5 链式调用类型
构建类型安全的链式调用 API:
class QueryBuilder<T extends Record<string, any>> {
private conditions: string[] = [];
where<K extends keyof T>(field: K, value: T[K]): this {
this.conditions.push(`${String(field)} = ${value}`);
return this;
}
build(): string {
return this.conditions.join(' AND ');
}
}
interface UserQuery {
name: string;
age: number;
email: string;
}
const query = new QueryBuilder<UserQuery>()
.where('name', 'Alice')
.where('age', 30)
.build();
// 类型安全:只能使用 UserQuery 的字段五、infer 关键字
infer 关键字用于在条件类型中声明类型变量,让 TypeScript 自动推断类型。
5.1 基础 infer 用法
// 提取函数参数类型
type Params<T> = T extends (...args: infer P) => any ? P : never;
type FnParams = Params<(a: string, b: number) => void>;
// [string, number]
// 提取 Promise 内部类型
type Unwrap<T> = T extends Promise<infer U> ? U : T;
type Unwrapped = Unwrap<Promise<string>>;
// string5.2 递归 infer
// 递归展开嵌套 Promise
type DeepUnwrap<T> = T extends Promise<infer U>
? DeepUnwrap<U>
: T;
type Nested = DeepUnwrap<Promise<Promise<Promise<string>>>>;
// string
// 提取数组元素类型(支持嵌套数组)
type Flatten<T> = T extends Array<infer U> ? Flatten<U> : T;
type Flat = Flatten<number[][][]>;
// number5.3 字符串解析
// 解析 CSS 长度值
type ParseLength<T extends string> =
T extends `${infer Num}${'px' | 'em' | 'rem' | '%'}`
? Num extends `${number}`
? { value: number; unit: T extends `${string}${infer Unit extends 'px' | 'em' | 'rem' | '%'}` ? Unit : never }
: never
: never;
type Length = ParseLength<'16px'>;
// { value: number; unit: 'px' }六、模板字面量类型
模板字面量类型基于字符串字面量类型构建,可以与联合类型组合产生强大的模式匹配能力。
6.1 基础用法
type EventName<T extends string> = `on${Capitalize<T}>`;
type ClickEvent = EventName<'click'>;
// 'onClick'
type MouseEvent = EventName<'mouseenter'>;
// 'onMouseEnter'6.2 联合类型展开
当模板字面量中使用联合类型时,会产生所有可能的组合:
type Vertical = 'top' | 'bottom';
type Horizontal = 'left' | 'right';
type Position = `${Vertical}-${Horizontal}`;
// 'top-left' | 'top-right' | 'bottom-left' | 'bottom-right'6.3 字符串操作工具类型
type UpperCaseKeys<T> = {
[P in keyof T as `${Uppercase<string & P>}`]: T[P];
};
type LowercaseKeys<T> = {
[P in keyof T as `${Lowercase<string & P>}`]: T[P];
};
// 移除前缀
type RemovePrefix<T extends string, Prefix extends string> =
T extends `${Prefix}${infer Rest}` ? Rest : T;
type Name = RemovePrefix<'onClick', 'on'>;
// 'Click'6.4 路由匹配
type Route = '/user/:id' | '/user/:id/post/:postId';
type Method = 'GET' | 'POST' | 'PUT' | 'DELETE';
type APIRoute = `${Method} ${Route}`;
// 'GET /user/:id' | 'POST /user/:id' | ...七、类型安全的高级模式
7.1 Branded Types(品牌类型)
品牌类型通过交叉类型创建结构上不兼容的"名义类型":
type Brand<T, B> = T & { __brand: B };
type UserId = Brand<number, 'UserId'>;
type PostId = Brand<number, 'PostId'>;
function getUser(id: UserId) {
// 查询用户
}
function getPost(id: PostId) {
// 查询文章
}
const uid = 1 as UserId;
const pid = 1 as PostId;
getUser(uid); // 正确
getUser(pid); // 类型错误!防止了参数混淆7.2 satisfies 关键字
satisfies 运算符在保持具体类型推断的同时检查类型是否满足某个约束:
const config = {
api: 'https://api.example.com',
timeout: 5000,
retry: true,
} satisfies Record<string, string | number | boolean>;
// 具体类型仍被保留
config.api; // 类型为 string(而非 string | number | boolean)
config.timeout; // 类型为 number
config.retry; // 类型为 boolean
// 但如果某个值不满足约束,会报错
const badConfig = {
api: null, // 错误!null 不在 string | number | boolean 中
} satisfies Record<string, string | number | boolean>;7.3 断言签名
TypeScript 3.7 引入了断言签名,用于在类型层面表示函数会抛出异常而非返回 false:
function assertIsString(value: unknown): asserts value is string {
if (typeof value !== 'string') {
throw new Error('值不是字符串');
}
}
function processValue(value: unknown) {
assertIsString(value);
value.toUpperCase(); // 安全使用,TypeScript 知道此处 value 为 string
}
// 自定义类型守卫
type Person_ = {
name: string;
age: number;
};
function assertIsPerson(obj: unknown): asserts obj is Person_ {
if (typeof obj !== 'object' || obj === null) {
throw new Error('不是对象');
}
if (!('name' in obj) || !('age' in obj)) {
throw new Error('缺少必要属性');
}
}7.4 幻影类型(Phantom Types)
幻影类型是一种在类型层面携带额外信息但运行时不存在的数据类型:
type Phantom<T, Tag> = T & { readonly _phantom: Tag };
type Meters = Phantom<number, 'meters'>;
type Seconds = Phantom<number, 'seconds'>;
function addDistance(a: Meters, b: Meters): Meters {
return (a + b) as Meters;
}
const distance = 100 as Meters;
const time = 5 as Seconds;
addDistance(distance, distance); // 正确
addDistance(distance, time); // 类型错误!7.5 类型安全的建造者模式
class StringBuilder<T extends string = ''> {
private value: string;
constructor(initial: string = '') {
this.value = initial;
}
append<S extends string>(str: S): StringBuilder<`${T}${S}`> {
return new StringBuilder<`${T}${S}`>(this.value + str);
}
build(): T {
return this.value as T;
}
}
const hello = new StringBuilder()
.append('Hello')
.append(', ')
.append('World!')
.append(' ')
.build();
// hello 的类型为 'Hello, World! '八、综合实战示例
8.1 类型安全的事件系统
type EventMap = {
click: { x: number; y: number };
focus: { target: HTMLElement };
keydown: { key: string; ctrlKey: boolean };
};
class TypedEmitter<T extends Record<string, any>> {
private handlers = new Map<keyof T, Set<Function>>();
on<K extends keyof T>(event: K, handler: (data: T[K]) => void): this {
if (!this.handlers.has(event)) {
this.handlers.set(event, new Set());
}
this.handlers.get(event)!.add(handler);
return this;
}
emit<K extends keyof T>(event: K, data: T[K]): void {
this.handlers.get(event)?.forEach(handler => handler(data));
}
off<K extends keyof T>(event: K, handler: (data: T[K]) => void): void {
this.handlers.get(event)?.delete(handler);
}
}
const emitter = new TypedEmitter<EventMap>();
emitter.on('click', data => {
// data 类型为 { x: number; y: number }
console.log(data.x, data.y);
});
emitter.emit('click', { x: 100, y: 200 });8.2 类型安全的 Redux Reducer
type Action<T extends string, P = void> =
P extends void ? { type: T } : { type: T; payload: P };
type Actions =
| Action<'INCREMENT'>
| Action<'DECREMENT'>
| Action<'SET_VALUE', number>
| Action<'RESET'>;
type State = { count: number };
function reducer(state: State, action: Actions): State {
switch (action.type) {
case 'INCREMENT':
return { count: state.count + 1 };
case 'DECREMENT':
return { count: state.count - 1 };
case 'SET_VALUE':
// action.payload 在此分支中自动推断为 number
return { count: action.payload };
case 'RESET':
return { count: 0 };
default:
return state;
}
}九、总结
TypeScript 的类型系统提供了丰富的工具来构建类型安全的应用程序。掌握这些进阶特性不仅能帮助你写出更健壮的代码,还能提升开发效率:
| 特性 | 用途 | 难度 |
|---|---|---|
| 装饰器 | AOP 编程、日志、验证 | 中等 |
| 条件类型 | 类型分支选择 | 中等 |
| 映射类型 | 类型变换与属性操作 | 中等 |
| infer | 类型推断 | 困难 |
| 模板字面量类型 | 字符串模式匹配 | 中等 |
| Branded Types | 名义类型模拟 | 高级 |
| satisfies | 保持类型推断 | 中等 |
建议在实际项目中逐步引入这些特性,从简单的工具类型开始,逐渐过渡到复杂的类型体操,让类型系统真正为你的项目保驾护航。