为什么 TypeScript 5.x 值得你重新学习?
TypeScript 5.0 于 2023 年发布,带来了装饰器标准、const 泛型、satisfies 操作符等重磅特性。很多开发者仍停留在 interface + 泛型 的基础用法,却错过了这些能显著提升代码质量与开发体验的利器。本文将带你从零实践三个进阶特性,每个示例都完整可运行,并附上真实项目中的踩坑总结。
环境准备
确保你的项目使用 TypeScript 5.0 或更高版本:
npm install typescript@^5.4 --save-dev
在 tsconfig.json 中开启严格模式(推荐):
{
"compilerOptions": {
"target": "ES2022",
"module": "NodeNext",
"strict": true,
"experimentalDecorators": false,
"emitDecoratorMetadata": false
}
}
> 💡 注意:TypeScript 5.0 支持的是 TC39 标准装饰器,与旧版 experimentalDecorators 不同。若你使用 NestJS 等框架,可能需要保留 experimentalDecorators: true,但本文聚焦标准装饰器。
一、标准装饰器:更简洁的元编程
标准装饰器允许你以声明式的方式修改类、方法、访问器、属性。与旧版相比,它不再需要 experimentalDecorators,且类型推断更准确。
1.1 方法装饰器:自动日志与性能监控
假设我们要为关键方法添加执行时间日志。传统做法是手动包裹,现在可以用装饰器:
function logTime(target: any, context: ClassMethodDecoratorContext) {
const methodName = String(context.name);
return function (this: any, ...args: any[]) {
const start = performance.now();
const result = target.apply(this, args);
const end = performance.now();
console.log(`[${methodName}] 执行耗时: ${(end - start).toFixed(2)}ms`);
return result;
};
}
class DataService {
@logTime
fetchData(id: number) {
// 模拟耗时操作
const start = Date.now();
while (Date.now() - start < 50) {}
return { id, data: 'sample' };
}
}
const service = new DataService();
service.fetchData(1);
// 输出: [fetchData] 执行耗时: 50.12ms
关键点:
context.name是方法名,类型为string | symbol。- 返回的新函数会替换原方法,
this正确绑定。 - 标准装饰器不支持参数装饰器,如需参数校验可用其他方案。
1.2 类装饰器:自动注册路由
在 Node.js 后端中,我们常用类装饰器自动注册控制器:
const routeRegistry: Record<string, any> = {};
function Controller(basePath: string) {
return function <T extends { new (...args: any[]): {} }>(target: T, context: ClassDecoratorContext) {
const originalConstructor = target;
const className = context.name;
// 返回新类,注入路由注册逻辑
return class extends originalConstructor {
constructor(...args: any[]) {
super(...args);
// 扫描原型方法上的路由装饰器
const methods = Object.getOwnPropertyNames(originalConstructor.prototype);
methods.forEach(method => {
if (method === 'constructor') return;
const route = Reflect.getMetadata(`route:${method}`, originalConstructor.prototype);
if (route) {
const fullPath = `${basePath}${route.path}`;
routeRegistry[fullPath] = (this as any)[method].bind(this);
console.log(`注册路由: ${route.method} ${fullPath}`);
}
});
}
};
};
}
// 辅助装饰器(简化示例,实际需用 Reflect.metadata)
function Get(path: string) {
return function (target: any, context: ClassMethodDecoratorContext) {
Reflect.defineMetadata(`route:${String(context.name)}`, { method: 'GET', path }, target);
};
}
@Controller('/api')
class UserController {
@Get('/users')
getUsers() {
return [{ id: 1, name: 'Alice' }];
}
}
new UserController();
// 输出: 注册路由: GET /api/users
> 💡 注意:标准装饰器不直接提供元数据 API,需手动引入 reflect-metadata 或使用 WeakMap 存储。若项目已用旧装饰器,迁移需谨慎评估。
二、const 泛型:让类型推断更精确
const 泛型是 TypeScript 5.0 引入的语法,允许泛型参数像 as const 一样推断为最窄的字面量类型。
2.1 问题场景:元组推断丢失
没有 const 泛型时,数组字面量会被推断为 string[],丢失元组信息:
function createTuple<T extends readonly any[]>(items: T): T {
return items;
}
const tuple1 = createTuple(['a', 'b']); // 类型: string[]
// 期望: readonly ['a', 'b']
2.2 使用 const 泛型
只需在泛型参数前加 const:
function createTuple<const T extends readonly any[]>(items: T): T {
return items;
}
const tuple2 = createTuple(['a', 'b']); // 类型: readonly ['a', 'b']
实际应用:类型安全的路由定义
type Route<Path extends string, Method extends string> = {
path: Path;
method: Method;
};
function defineRoute<const T extends readonly Route<string, string>[]>(routes: T): T {
return routes;
}
const routes = defineRoute([
{ path: '/users', method: 'GET' },
{ path: '/users', method: 'POST' },
]);
// routes 类型: readonly [{ path: '/users'; method: 'GET'; }, { path: '/users'; method: 'POST'; }]
这样在后续代码中,routes[0].method 的类型是 'GET' 而非 string,可以用于条件类型或映射类型。
> 💡 注意:const 泛型只影响推断,不影响运行时。对于对象字面量,它会使属性变为 readonly,若需可变请显式标注。
三、satisfies 操作符:兼顾类型检查与推断
satisfies 允许你验证表达式是否符合某个类型,同时保留表达式最具体的类型。
3.1 传统痛点:类型注解丢失具体信息
type Colors = Record<string, string | [number, number, number]>;
const palette: Colors = {
red: '#ff0000',
green: [0, 255, 0],
};
// palette.red 的类型是 string | [number, number, number]
// 无法直接调用 string 方法
3.2 使用 satisfies
const palette = {
red: '#ff0000',
green: [0, 255, 0],
} satisfies Colors;
// palette.red 的类型是 string
// palette.green 的类型是 [number, number, number]
console.log(palette.red.toUpperCase()); // 合法
实际应用:配置对象校验
type Config = {
apiUrl: string;
timeout: number;
retries: number;
};
const config = {
apiUrl: 'https://api.example.com',
timeout: 5000,
retries: 3,
} satisfies Config;
// config.apiUrl 类型是 string,但更精确的是字面量类型?
// 实际上 satisfies 不会收窄到字面量,除非配合 as const
const config2 = {
apiUrl: 'https://api.example.com',
timeout: 5000,
retries: 3,
} as const satisfies Config;
// config2.apiUrl 类型: 'https://api.example.com'
> 💡 注意:satisfies 不改变运行时行为,它只是编译时检查。与 as const 结合时,注意顺序:as const satisfies Config 先断言再检查。
四、综合实战:类型安全的事件系统
将三个特性结合,构建一个类型安全的事件发射器:
type EventMap = {
login: { userId: string; timestamp: number };
logout: { userId: string };
error: { message: string; code: number };
};
class TypedEventEmitter<const T extends Record<string, any>> {
private listeners: { [K in keyof T]?: Array<(payload: T[K]) => void> } = {};
on<K extends keyof T>(event: K, listener: (payload: T[K]) => void) {
if (!this.listeners[event]) {
this.listeners[event] = [];
}
this.listeners[event]!.push(listener);
}
emit<K extends keyof T>(event: K, payload: T[K]) {
this.listeners[event]?.forEach(listener => listener(payload));
}
}
// 使用 const 泛型确保事件名和负载类型精确
const emitter = new TypedEventEmitter<{
login: { userId: string; timestamp: number };
logout: { userId: string };
error: { message: string; code: number };
}>();
emitter.on('login', (payload) => {
console.log(payload.userId, payload.timestamp); // 类型安全
});
emitter.emit('login', { userId: 'u1', timestamp: Date.now() });
// emitter.emit('login', { userId: 'u1' }); // 错误:缺少 timestamp
结合装饰器自动注册事件监听(略,可扩展)。
常见坑与最佳实践
- 装饰器迁移:旧项目升级到标准装饰器时,参数装饰器不可用。若必须使用,可暂时保留
experimentalDecorators,但新代码建议用标准装饰器。 - const 泛型与可变性:
const泛型会使推断出的对象属性变为readonly,若后续需要修改,请显式声明类型或使用satisfies。 - satisfies 与类型断言:
satisfies不会改变类型,只是检查。若需要更宽泛的类型,仍应使用类型注解。 - 性能考虑:装饰器在运行时增加少量开销,但通常可忽略。对于高频调用的方法,谨慎使用。
总结
TypeScript 5.x 的装饰器、const 泛型和 satisfies 操作符,分别从元编程、类型推断和类型检查三个维度提升了开发体验。掌握它们,你可以写出更简洁、更安全、更易维护的代码。建议在下一个新项目中尝试这些特性,逐步替换旧模式。
延伸阅读:
- TypeScript 官方发布说明:https://devblogs.microsoft.com/typescript/announcing-typescript-5-0/
- TC39 装饰器提案:https://github.com/tc39/proposal-decorators
- 类型体操进阶:条件类型与映射类型