跳到主要内容

TypeScript 装饰器

装饰器是一种特殊的声明,可以附加到类声明、方法、访问器、属性或参数上,用于修改或添加功能。装饰器提供了一种优雅的方式来实现元编程。

启用装饰器

装饰器目前是实验性功能,需要在 tsconfig.json 中启用:

{
"compilerOptions": {
"experimentalDecorators": true,
"emitDecoratorMetadata": true // 可选,用于元数据
}
}

注意:TypeScript 5.0 开始支持 ECMAScript Stage 3 装饰器标准,语法有所不同。本章介绍经典的实验性装饰器。

装饰器基础

装饰器使用 @expression 形式,其中 expression 必须求值为一个函数,该函数在运行时被调用,并传入被装饰声明的信息。

function sealed(constructor: Function) {
Object.seal(constructor);
Object.seal(constructor.prototype);
}

@sealed
class BugReport {
type = "report";
title: string;

constructor(t: string) {
this.title = t;
}
}

解释@sealed 装饰器会密封类及其原型,防止运行时添加或删除属性。

装饰器工厂

装饰器工厂是一个返回装饰器函数的函数,用于自定义装饰器的行为:

function color(value: string) {
// 装饰器工厂
return function (target: any, propertyKey: string) {
// 装饰器函数
console.log(`${propertyKey} 的颜色设置为 ${value}`);
};
}

class Button {
@color("blue")
background: string;
}
// 输出:background 的颜色设置为 blue

解释:装饰器工厂允许传递参数,使装饰器更灵活可配置。

装饰器组合

多个装饰器可以同时应用到一个声明上:

// 单行写法
@f @g x

// 多行写法
@f
@g
x

执行顺序

装饰器的执行遵循数学函数组合的规则 (f ∘ g)(x) 等价于 f(g(x))

  1. 从上到下评估每个装饰器表达式(工厂函数执行)
  2. 从下到上调用装饰器函数
function first() {
console.log("first(): 工厂评估");
return function (
target: any,
propertyKey: string,
descriptor: PropertyDescriptor
) {
console.log("first(): 被调用");
};
}

function second() {
console.log("second(): 工厂评估");
return function (
target: any,
propertyKey: string,
descriptor: PropertyDescriptor
) {
console.log("second(): 被调用");
};
}

class ExampleClass {
@first()
@second()
method() {}
}

// 输出顺序:
// first(): 工厂评估
// second(): 工厂评估
// second(): 被调用
// first(): 被调用

类中装饰器的执行顺序

  1. 参数装饰器,然后是方法/访问器/属性装饰器,应用于每个实例成员
  2. 参数装饰器,然后是方法/访问器/属性装饰器,应用于每个静态成员
  3. 参数装饰器应用于构造函数
  4. 类装饰器应用于类

类装饰器

类装饰器在类声明之前声明,应用于类的构造函数,可以观察、修改或替换类定义。

function sealed(constructor: Function) {
Object.seal(constructor);
Object.seal(constructor.prototype);
}

@sealed
class BugReport {
type = "report";
title: string;

constructor(t: string) {
this.title = t;
}
}

替换类构造函数

类装饰器可以返回一个新的构造函数来替换原始类:

function reportableClassDecorator<T extends { new (...args: any[]): {} }>(
constructor: T
) {
return class extends constructor {
reportingURL = "http://www.example.com/report";
};
}

@reportableClassDecorator
class BugReport {
type = "report";
title: string;

constructor(t: string) {
this.title = t;
}
}

const bug = new BugReport("需要暗黑模式");
console.log(bug.title); // "需要暗黑模式"
console.log(bug.type); // "report"
// console.log(bug.reportingURL); // 存在,但 TypeScript 类型不知道

解释:装饰器返回的新类不会改变 TypeScript 的类型推断,所以需要额外声明类型。

实际应用示例

// 日志装饰器
function logged(constructor: any) {
console.log(`${constructor.name} 已被创建`);

// 保存原始构造函数
const original = constructor;

// 创建新的构造函数
const newConstructor: any = function (...args: any[]) {
console.log(`创建 ${original.name} 实例,参数:`, args);
return new original(...args);
};

// 复制原型
newConstructor.prototype = original.prototype;
return newConstructor;
}

@logged
class Person {
constructor(public name: string, public age: number) {}
}

const person = new Person("张三", 25);
// 输出:
// 类 Person 已被创建
// 创建 Person 实例,参数: ["张三", 25]

方法装饰器

方法装饰器在方法声明之前声明,应用于方法的属性描述符。

参数:

  1. 对于静态成员是类的构造函数,对于实例成员是类的原型
  2. 成员名称
  3. 成员的属性描述符
function enumerable(value: boolean) {
return function (
target: any,
propertyKey: string,
descriptor: PropertyDescriptor
) {
descriptor.enumerable = value;
};
}

class Greeter {
greeting: string;

constructor(message: string) {
this.greeting = message;
}

@enumerable(false)
greet() {
return "Hello, " + this.greeting;
}
}

const greeter = new Greeter("world");
for (const key in greeter) {
console.log(key); // 只输出 "greeting",不输出 "greet"
}

方法执行时机装饰器

function measure(
target: any,
propertyKey: string,
descriptor: PropertyDescriptor
) {
const originalMethod = descriptor.value;

descriptor.value = function (...args: any[]) {
const start = performance.now();
const result = originalMethod.apply(this, args);
const end = performance.now();
console.log(`${propertyKey} 执行耗时: ${(end - start).toFixed(2)}ms`);
return result;
};

return descriptor;
}

class Calculator {
@measure
fibonacci(n: number): number {
if (n <= 1) return n;
return this.fibonacci(n - 1) + this.fibonacci(n - 2);
}
}

const calc = new Calculator();
calc.fibonacci(30); // 输出执行耗时

方法缓存装饰器

function memoize(
target: any,
propertyKey: string,
descriptor: PropertyDescriptor
) {
const originalMethod = descriptor.value;
const cache = new Map<string, any>();

descriptor.value = function (...args: any[]) {
const key = JSON.stringify(args);
if (cache.has(key)) {
console.log(`从缓存返回 ${propertyKey}(${key})`);
return cache.get(key);
}

const result = originalMethod.apply(this, args);
cache.set(key, result);
return result;
};

return descriptor;
}

class MathOperations {
@memoize
fibonacci(n: number): number {
if (n <= 1) return n;
return this.fibonacci(n - 1) + this.fibonacci(n - 2);
}
}

const math = new MathOperations();
math.fibonacci(10); // 计算
math.fibonacci(10); // 从缓存返回

访问器装饰器

访问器装饰器应用于访问器的属性描述符。注意:TypeScript 不允许同时装饰 getset,所有装饰器必须应用于文档顺序中的第一个访问器。

function configurable(value: boolean) {
return function (
target: any,
propertyKey: string,
descriptor: PropertyDescriptor
) {
descriptor.configurable = value;
};
}

class Point {
private _x: number;
private _y: number;

constructor(x: number, y: number) {
this._x = x;
this._y = y;
}

@configurable(false)
get x() {
return this._x;
}

@configurable(false)
get y() {
return this._y;
}
}

属性装饰器

属性装饰器在属性声明之前声明。

参数:

  1. 对于静态成员是类的构造函数,对于实例成员是类的原型
  2. 成员名称

注意:属性描述符不作为参数提供,因为 TypeScript 没有机制在定义原型成员时描述实例属性。返回值也会被忽略。

import "reflect-metadata";

const formatMetadataKey = Symbol("format");

function format(formatString: string) {
return Reflect.metadata(formatMetadataKey, formatString);
}

function getFormat(target: any, propertyKey: string) {
return Reflect.getMetadata(formatMetadataKey, target, propertyKey);
}

class Greeter {
@format("Hello, %s")
greeting: string;

constructor(message: string) {
this.greeting = message;
}

greet() {
const formatString = getFormat(this, "greeting");
return formatString.replace("%s", this.greeting);
}
}

const greeter = new Greeter("世界");
console.log(greeter.greet()); // "Hello, 世界"

属性验证装饰器

function required(target: object, propertyKey: string | symbol) {
// 注册需要验证的属性
const requiredKeys = Reflect.getOwnMetadata("required", target) || [];
requiredKeys.push(propertyKey);
Reflect.defineMetadata("required", requiredKeys, target);
}

function validate(
target: any,
propertyKey: string,
descriptor: PropertyDescriptor
) {
const originalMethod = descriptor.value;

descriptor.value = function (...args: any[]) {
const requiredKeys = Reflect.getOwnMetadata("required", this) || [];

for (const key of requiredKeys) {
if (this[key] === undefined || this[key] === null) {
throw new Error(`属性 ${String(key)} 是必需的`);
}
}

return originalMethod.apply(this, args);
};

return descriptor;
}

class User {
@required
name!: string;

@required
email!: string;

age?: number;

@validate
save() {
console.log(`保存用户: ${this.name}`);
}
}

const user = new User();
user.name = "张三";
// user.email = "[email protected]"; // 忘记设置
// user.save(); // 错误:属性 email 是必需的

参数装饰器

参数装饰器在参数声明之前声明。

参数:

  1. 对于静态成员是类的构造函数,对于实例成员是类的原型
  2. 成员名称
  3. 参数在函数参数列表中的索引

参数装饰器只能用于观察参数是否被声明,返回值被忽略。

import "reflect-metadata";

const requiredMetadataKey = Symbol("required");

function required(
target: Object,
propertyKey: string | symbol,
parameterIndex: number
) {
const existingRequiredParameters: number[] =
Reflect.getOwnMetadata(requiredMetadataKey, target, propertyKey) || [];
existingRequiredParameters.push(parameterIndex);
Reflect.defineMetadata(
requiredMetadataKey,
existingRequiredParameters,
target,
propertyKey
);
}

function validate(
target: any,
propertyName: string,
descriptor: TypedPropertyDescriptor<Function>
) {
const method = descriptor.value!;

descriptor.value = function (...args: any[]) {
const requiredParameters: number[] = Reflect.getOwnMetadata(
requiredMetadataKey,
target,
propertyName
);

if (requiredParameters) {
for (const parameterIndex of requiredParameters) {
if (
parameterIndex >= args.length ||
args[parameterIndex] === undefined
) {
throw new Error("缺少必需参数");
}
}
}

return method.apply(this, args);
};
}

class BugReport {
type = "report";
title: string;

constructor(t: string) {
this.title = t;
}

@validate
print(@required verbose: boolean) {
if (verbose) {
return `type: ${this.type}\ntitle: ${this.title}`;
} else {
return this.title;
}
}
}

const report = new BugReport("测试报告");
// report.print(undefined as any); // 错误:缺少必需参数
report.print(true); // OK

元数据

reflect-metadata 库提供了元数据 API 的 polyfill:

npm install reflect-metadata

在入口文件导入:

import "reflect-metadata";

使用元数据

import "reflect-metadata";

const metadataKey = Symbol("design:type");

class Example {
// TypeScript 自动注入设计时类型信息
name: string = "hello";

method(value: number): string {
return value.toString();
}
}

const example = new Example();

// 获取属性类型
const type = Reflect.getMetadata("design:type", example, "name");
console.log(type); // [Function: String]

// 获取返回类型
const returnType = Reflect.getMetadata("design:returntype", example, "method");
console.log(returnType); // [Function: String]

// 获取参数类型
const paramTypes = Reflect.getMetadata("design:paramtypes", example, "method");
console.log(paramTypes); // [[Function: Number]]

启用元数据发射

tsconfig.json 中:

{
"compilerOptions": {
"experimentalDecorators": true,
"emitDecoratorMetadata": true
}
}

实际应用场景

1. 依赖注入

import "reflect-metadata";

type Constructor<T = any> = new (...args: any[]) => T;

const INJECTABLE_TOKEN = Symbol("injectable");

function Injectable() {
return function (target: Constructor) {
Reflect.defineMetadata(INJECTABLE_TOKEN, true, target);
};
}

function Inject(token?: string) {
return function (
target: any,
propertyKey: string | symbol | undefined,
parameterIndex: number
) {
const existingTokens =
Reflect.getOwnMetadata("inject:tokens", target) || [];
existingTokens[parameterIndex] = token;
Reflect.defineMetadata("inject:tokens", existingTokens, target);
};
}

@Injectable()
class DatabaseService {
query(sql: string) {
console.log(`执行查询: ${sql}`);
return [{ id: 1, name: "张三" }];
}
}

@Injectable()
class UserService {
constructor(@Inject() private db: DatabaseService) {}

getUsers() {
return this.db.query("SELECT * FROM users");
}
}

// 简单的容器
class Container {
private instances = new Map();

resolve<T>(target: Constructor<T>): T {
if (this.instances.has(target)) {
return this.instances.get(target);
}

const paramTypes: Constructor[] =
Reflect.getMetadata("design:paramtypes", target) || [];
const injections = paramTypes.map((param) => this.resolve(param));

const instance = new target(...injections);
this.instances.set(target, instance);
return instance;
}
}

const container = new Container();
const userService = container.resolve(UserService);
userService.getUsers(); // 执行查询: SELECT * FROM users

2. API 路由装饰器

import "reflect-metadata";

const METHOD_METADATA = Symbol("method");
const PATH_METADATA = Symbol("path");

function Get(path: string): MethodDecorator {
return (
target: any,
propertyKey: string | symbol,
descriptor: PropertyDescriptor
) => {
Reflect.defineMetadata(METHOD_METADATA, "GET", descriptor.value);
Reflect.defineMetadata(PATH_METADATA, path, descriptor.value);
};
}

function Post(path: string): MethodDecorator {
return (
target: any,
propertyKey: string | symbol,
descriptor: PropertyDescriptor
) => {
Reflect.defineMetadata(METHOD_METADATA, "POST", descriptor.value);
Reflect.defineMetadata(PATH_METADATA, path, descriptor.value);
};
}

function Controller(path: string): ClassDecorator {
return (target: any) => {
Reflect.defineMetadata(PATH_METADATA, path, target);
};
}

@Controller("/users")
class UserController {
@Get("/")
getAll() {
return { users: [] };
}

@Get("/:id")
getById(id: string) {
return { id, name: "张三" };
}

@Post("/")
create(data: any) {
return { success: true, data };
}
}

// 收集路由
function registerRoutes(controller: any) {
const basePath = Reflect.getMetadata(PATH_METADATA, controller.constructor);
const methods = Object.getOwnPropertyNames(
Object.getPrototypeOf(controller)
).filter((m) => m !== "constructor");

const routes: any[] = [];

for (const methodName of methods) {
const method = (controller as any)[methodName];
const httpMethod = Reflect.getMetadata(METHOD_METADATA, method);
const path = Reflect.getMetadata(PATH_METADATA, method);

if (httpMethod && path) {
routes.push({
method: httpMethod,
path: basePath + path,
handler: method.bind(controller),
});
}
}

return routes;
}

const controller = new UserController();
const routes = registerRoutes(controller);
console.log(routes);
// [
// { method: 'GET', path: '/users/', handler: [Function: getAll] },
// { method: 'GET', path: '/users/:id', handler: [Function: getById] },
// { method: 'POST', path: '/users/', handler: [Function: create] }
// ]

3. 数据验证装饰器

import "reflect-metadata";

function validateType(expectedType: string) {
return function (target: any, propertyKey: string) {
const existingValidations =
Reflect.getMetadata("validations", target) || {};
existingValidations[propertyKey] = {
...existingValidations[propertyKey],
type: expectedType,
};
Reflect.defineMetadata("validations", existingValidations, target);
};
}

function minLength(length: number) {
return function (target: any, propertyKey: string) {
const existingValidations =
Reflect.getMetadata("validations", target) || {};
existingValidations[propertyKey] = {
...existingValidations[propertyKey],
minLength: length,
};
Reflect.defineMetadata("validations", existingValidations, target);
};
}

function maxLength(length: number) {
return function (target: any, propertyKey: string) {
const existingValidations =
Reflect.getMetadata("validations", target) || {};
existingValidations[propertyKey] = {
...existingValidations[propertyKey],
maxLength: length,
};
Reflect.defineMetadata("validations", existingValidations, target);
};
}

function range(min: number, max: number) {
return function (target: any, propertyKey: string) {
const existingValidations =
Reflect.getMetadata("validations", target) || {};
existingValidations[propertyKey] = {
...existingValidations[propertyKey],
range: { min, max },
};
Reflect.defineMetadata("validations", existingValidations, target);
};
}

function validate<T extends object>(obj: T): { valid: boolean; errors: string[] } {
const validations = Reflect.getMetadata("validations", obj) || {};
const errors: string[] = [];

for (const [property, rules] of Object.entries(validations) as [string, any][]) {
const value = (obj as any)[property];

if (rules.type && typeof value !== rules.type) {
errors.push(`${property} 必须是 ${rules.type} 类型`);
}

if (rules.minLength && value.length < rules.minLength) {
errors.push(`${property} 长度不能少于 ${rules.minLength}`);
}

if (rules.maxLength && value.length > rules.maxLength) {
errors.push(`${property} 长度不能超过 ${rules.maxLength}`);
}

if (rules.range && (value < rules.range.min || value > rules.range.max)) {
errors.push(
`${property} 必须在 ${rules.range.min}${rules.range.max} 之间`
);
}
}

return { valid: errors.length === 0, errors };
}

class User {
@validateType("string")
@minLength(2)
@maxLength(20)
name!: string;

@validateType("number")
@range(0, 150)
age!: number;

@validateType("string")
@minLength(5)
@maxLength(100)
email!: string;
}

const user = new User();
user.name = "张";
user.age = 200;
user.email = "a@b";

const result = validate(user);
console.log(result);
// {
// valid: false,
// errors: [
// 'name 长度不能少于 2',
// 'age 必须在 0 到 150 之间',
// 'email 长度不能少于 5'
// ]
// }

小结

本章我们学习了 TypeScript 装饰器:

  1. 装饰器基础:装饰器的概念和启用方式
  2. 装饰器工厂:可配置的装饰器
  3. 装饰器组合:多个装饰器的执行顺序
  4. 类装饰器:修改或替换类定义
  5. 方法装饰器:修改方法行为
  6. 访问器装饰器:修改 getter/setter
  7. 属性装饰器:添加属性元数据
  8. 参数装饰器:验证方法参数
  9. 元数据:使用 reflect-metadata
  10. 实际应用:依赖注入、路由、验证等

练习

  1. 创建一个 @log 方法装饰器,记录方法调用和参数
  2. 创建一个 @readonly 类装饰器,使所有属性只读
  3. 创建一个 @delay(ms) 方法装饰器,延迟方法执行
  4. 实现一个简单的依赖注入容器,使用装饰器注入服务