TypeScript 5.x 进阶用法:装饰器、const 泛型与 satisfies 实战

By | 2026年10月3日

为什么 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

结合装饰器自动注册事件监听(略,可扩展)。

常见坑与最佳实践

  1. 装饰器迁移:旧项目升级到标准装饰器时,参数装饰器不可用。若必须使用,可暂时保留 experimentalDecorators,但新代码建议用标准装饰器。
  2. const 泛型与可变性:const 泛型会使推断出的对象属性变为 readonly,若后续需要修改,请显式声明类型或使用 satisfies。
  3. satisfies 与类型断言:satisfies 不会改变类型,只是检查。若需要更宽泛的类型,仍应使用类型注解。
  4. 性能考虑:装饰器在运行时增加少量开销,但通常可忽略。对于高频调用的方法,谨慎使用。

总结

TypeScript 5.x 的装饰器、const 泛型和 satisfies 操作符,分别从元编程、类型推断和类型检查三个维度提升了开发体验。掌握它们,你可以写出更简洁、更安全、更易维护的代码。建议在下一个新项目中尝试这些特性,逐步替换旧模式。

延伸阅读:

  • TypeScript 官方发布说明:https://devblogs.microsoft.com/typescript/announcing-typescript-5-0/
  • TC39 装饰器提案:https://github.com/tc39/proposal-decorators
  • 类型体操进阶:条件类型与映射类型