跳到主要内容

装饰器

问题

什么是 TypeScript 装饰器?如何使用类装饰器、方法装饰器、属性装饰器和参数装饰器?

答案

装饰器是一种特殊的声明,可以附加到类、方法、属性或参数上,用于修改或增强它们的行为。装饰器是一种元编程技术,在框架中广泛使用(如 Angular、NestJS)。

注意

TypeScript 5.0 引入了新的装饰器语法(ECMAScript 标准),与之前的实验性装饰器(experimentalDecorators)不同。本文主要介绍标准装饰器和实验性装饰器。


启用装饰器

tsconfig.json
{
"compilerOptions": {
// 实验性装饰器(TypeScript 4.x 及之前)
"experimentalDecorators": true,
"emitDecoratorMetadata": true,

// TypeScript 5.0+ 标准装饰器无需配置
}
}

类装饰器

基本用法

// 实验性装饰器
function Logger(constructor: Function) {
console.log('Creating instance of:', constructor.name);
}

@Logger
class User {
constructor(public name: string) {}
}

// 输出: "Creating instance of: User"
const user = new User('Alice');

装饰器工厂

function Logger(prefix: string) {
return function (constructor: Function) {
console.log(`${prefix}: ${constructor.name}`);
};
}

@Logger('CLASS')
class User {
name = 'Alice';
}
// 输出: "CLASS: User"

修改类

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

function AddTimestamp<T extends { new (...args: any[]): object }>(
constructor: T
) {
return class extends constructor {
createdAt = new Date();
};
}

@Sealed
@AddTimestamp
class User {
constructor(public name: string) {}
}

const user = new User('Alice');
console.log((user as any).createdAt); // 当前时间

方法装饰器

基本用法

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

descriptor.value = function (...args: any[]) {
console.log(`Calling ${propertyKey} with:`, args);
const result = originalMethod.apply(this, args);
console.log(`Result:`, result);
return result;
};

return descriptor;
}

class Calculator {
@Log
add(a: number, b: number): number {
return a + b;
}
}

const calc = new Calculator();
calc.add(2, 3);
// 输出:
// Calling add with: [2, 3]
// Result: 5

缓存装饰器

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

descriptor.value = function (...args: any[]) {
const key = JSON.stringify(args);
if (cache.has(key)) {
console.log('Cache hit');
return cache.get(key);
}

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

return descriptor;
}

class DataService {
@Memoize
fetchData(id: number): string {
console.log('Fetching...');
return `Data for ${id}`;
}
}

const service = new DataService();
service.fetchData(1); // Fetching...
service.fetchData(1); // Cache hit

错误处理装饰器

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

descriptor.value = async function (...args: any[]) {
try {
return await originalMethod.apply(this, args);
} catch (error) {
console.error(`Error in ${propertyKey}:`, error);
throw error;
}
};

return descriptor;
}

class API {
@CatchError
async fetchUser(id: number) {
const response = await fetch(`/api/users/${id}`);
if (!response.ok) throw new Error('Failed to fetch');
return response.json();
}
}

属性装饰器

function MinLength(length: number) {
return function (target: any, propertyKey: string) {
let value: string;

const getter = () => value;
const setter = (newValue: string) => {
if (newValue.length < length) {
throw new Error(
`${propertyKey} must be at least ${length} characters`
);
}
value = newValue;
};

Object.defineProperty(target, propertyKey, {
get: getter,
set: setter,
enumerable: true,
configurable: true
});
};
}

class User {
@MinLength(3)
name!: string;
}

const user = new User();
user.name = 'Al'; // 错误:name must be at least 3 characters
user.name = 'Alice'; // OK

响应式属性

function Observable(target: any, propertyKey: string) {
const privateKey = `_${propertyKey}`;
const listeners: Array<(value: any) => void> = [];

Object.defineProperty(target, propertyKey, {
get() {
return this[privateKey];
},
set(value) {
this[privateKey] = value;
listeners.forEach((listener) => listener(value));
}
});

target[`on${propertyKey.charAt(0).toUpperCase() + propertyKey.slice(1)}Change`] =
(callback: (value: any) => void) => {
listeners.push(callback);
};
}

class Store {
@Observable
count = 0;
}

const store = new Store() as any;
store.onCountChange((value: number) => console.log('Count changed:', value));
store.count = 5; // Count changed: 5

参数装饰器

const requiredParams: Map<Function, number[]> = new Map();

function Required(
target: any,
propertyKey: string,
parameterIndex: number
) {
const existing = requiredParams.get(target[propertyKey]) || [];
existing.push(parameterIndex);
requiredParams.set(target[propertyKey], existing);
}

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

descriptor.value = function (...args: any[]) {
const required = requiredParams.get(originalMethod) || [];
for (const index of required) {
if (args[index] === undefined || args[index] === null) {
throw new Error(`Parameter at index ${index} is required`);
}
}
return originalMethod.apply(this, args);
};

return descriptor;
}

class UserService {
@Validate
createUser(@Required name: string, age?: number) {
return { name, age };
}
}

const service = new UserService();
service.createUser('Alice', 25); // OK
service.createUser(null as any); // 错误:Parameter at index 0 is required

TypeScript 5.0+ 标准装饰器

类装饰器

type ClassDecorator = <T extends new (...args: any[]) => any>(
target: T,
context: ClassDecoratorContext<T>
) => T | void;

function Logged<T extends new (...args: any[]) => any>(
target: T,
context: ClassDecoratorContext<T>
) {
console.log(`Creating class: ${context.name}`);
return target;
}

@Logged
class User {
name = 'Alice';
}

方法装饰器

type MethodDecorator = <T extends (...args: any[]) => any>(
target: T,
context: ClassMethodDecoratorContext
) => T | void;

function LogMethod<T extends (...args: any[]) => any>(
target: T,
context: ClassMethodDecoratorContext
): T {
const methodName = String(context.name);

function replacementMethod(this: any, ...args: any[]): any {
console.log(`Calling ${methodName} with args:`, args);
const result = target.call(this, ...args);
console.log(`${methodName} returned:`, result);
return result;
}

return replacementMethod as T;
}

class Calculator {
@LogMethod
add(a: number, b: number): number {
return a + b;
}
}

字段装饰器

function DefaultValue<T>(value: T) {
return function (
target: undefined,
context: ClassFieldDecoratorContext
) {
return function (initialValue: T): T {
return initialValue ?? value;
};
};
}

class Settings {
@DefaultValue('guest')
username!: string;

@DefaultValue(8080)
port!: number;
}

const settings = new Settings();
console.log(settings.username); // 'guest'
console.log(settings.port); // 8080

Accessor 装饰器

function Logged<T, V>(
target: ClassAccessorDecoratorTarget<T, V>,
context: ClassAccessorDecoratorContext<T, V>
): ClassAccessorDecoratorResult<T, V> {
return {
get(this: T): V {
console.log(`Getting ${String(context.name)}`);
return target.get.call(this);
},
set(this: T, value: V): void {
console.log(`Setting ${String(context.name)} to`, value);
target.set.call(this, value);
}
};
}

class User {
@Logged
accessor name = 'Alice';
}

装饰器组合

装饰器从上到下求值,从下到上执行:

function First() {
console.log('First(): evaluated');
return function (
target: any,
propertyKey: string,
descriptor: PropertyDescriptor
) {
console.log('First(): called');
};
}

function Second() {
console.log('Second(): evaluated');
return function (
target: any,
propertyKey: string,
descriptor: PropertyDescriptor
) {
console.log('Second(): called');
};
}

class Example {
@First()
@Second()
method() {}
}

// 输出:
// First(): evaluated
// Second(): evaluated
// Second(): called
// First(): called

实际应用场景

依赖注入

const container = new Map<string, any>();

function Injectable(token: string) {
return function (constructor: Function) {
container.set(token, new (constructor as any)());
};
}

function Inject(token: string) {
return function (target: any, propertyKey: string) {
Object.defineProperty(target, propertyKey, {
get: () => container.get(token),
enumerable: true
});
};
}

@Injectable('userService')
class UserService {
getUser() {
return { name: 'Alice' };
}
}

class Controller {
@Inject('userService')
userService!: UserService;

handleRequest() {
return this.userService.getUser();
}
}

const controller = new Controller();
console.log(controller.handleRequest()); // { name: 'Alice' }

路由装饰器

const routes: Array<{ method: string; path: string; handler: Function }> = [];

function Controller(basePath: string) {
return function (constructor: Function) {
(constructor as any).basePath = basePath;
};
}

function Get(path: string) {
return function (
target: any,
propertyKey: string,
descriptor: PropertyDescriptor
) {
routes.push({
method: 'GET',
path: path,
handler: descriptor.value
});
};
}

function Post(path: string) {
return function (
target: any,
propertyKey: string,
descriptor: PropertyDescriptor
) {
routes.push({
method: 'POST',
path: path,
handler: descriptor.value
});
};
}

@Controller('/users')
class UserController {
@Get('/')
getUsers() {
return ['Alice', 'Bob'];
}

@Post('/')
createUser() {
return { created: true };
}
}

console.log(routes);
// [
// { method: 'GET', path: '/', handler: [Function] },
// { method: 'POST', path: '/', handler: [Function] }
// ]

常见面试问题

Q1: 装饰器的执行顺序是什么?

答案

function ClassDec() {
console.log('ClassDec: evaluated');
return (constructor: Function) => console.log('ClassDec: executed');
}

function MethodDec() {
console.log('MethodDec: evaluated');
return (t: any, k: string, d: PropertyDescriptor) =>
console.log('MethodDec: executed');
}

function PropDec() {
console.log('PropDec: evaluated');
return (t: any, k: string) => console.log('PropDec: executed');
}

function ParamDec() {
console.log('ParamDec: evaluated');
return (t: any, k: string, i: number) => console.log('ParamDec: executed');
}

@ClassDec()
class Example {
@PropDec()
prop = 1;

@MethodDec()
method(@ParamDec() arg: string) {}
}

// 执行顺序:
// 1. 属性装饰器:PropDec evaluated -> PropDec executed
// 2. 参数装饰器:ParamDec evaluated -> ParamDec executed
// 3. 方法装饰器:MethodDec evaluated -> MethodDec executed
// 4. 类装饰器:ClassDec evaluated -> ClassDec executed

// 多个装饰器:从上到下求值,从下到上执行

Q2: 实验性装饰器和标准装饰器的区别?

答案

特性实验性装饰器标准装饰器 (TS 5.0+)
配置需要 experimentalDecorators无需配置
参数(target, key, descriptor)(target, context)
元数据支持 emitDecoratorMetadata不支持
参数装饰器支持不支持
Accessor不支持支持 accessor 关键字
规范旧提案ECMAScript 标准
// 实验性装饰器
function OldLog(
target: any,
key: string,
descriptor: PropertyDescriptor
) {
// target: 类的原型
// key: 方法名
// descriptor: 属性描述符
}

// 标准装饰器
function NewLog<T extends (...args: any[]) => any>(
target: T,
context: ClassMethodDecoratorContext
): T {
// target: 方法本身
// context: 包含名称、类型等元数据
return target;
}

Q3: 如何用装饰器实现方法执行时间统计?

答案

// 实验性装饰器版本
function Timing(
target: any,
propertyKey: string,
descriptor: PropertyDescriptor
) {
const originalMethod = descriptor.value;

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

return descriptor;
}

// 标准装饰器版本
function TimingNew<T extends (...args: any[]) => any>(
target: T,
context: ClassMethodDecoratorContext
): T {
const methodName = String(context.name);

async function replacement(this: any, ...args: any[]): Promise<any> {
const start = performance.now();
const result = await target.apply(this, args);
const end = performance.now();
console.log(`${methodName} took ${(end - start).toFixed(2)}ms`);
return result;
}

return replacement as T;
}

class DataService {
@Timing
async fetchData(id: number) {
await new Promise((r) => setTimeout(r, 100));
return { id };
}
}

Q4: 装饰器能改变类型吗?

答案

// 装饰器可以修改运行时行为,但 TypeScript 类型不会自动更新

function AddCreatedAt<T extends new (...args: any[]) => object>(
constructor: T
) {
return class extends constructor {
createdAt = new Date();
};
}

@AddCreatedAt
class User {
name = 'Alice';
}

const user = new User();
// user.createdAt; // 运行时存在,但 TypeScript 不知道

// 解决方案 1:类型断言
console.log((user as any).createdAt);

// 解决方案 2:接口合并
interface User {
createdAt: Date;
}

@AddCreatedAt
class User {
name = 'Alice';
}

// 解决方案 3:返回新类型的工厂函数
function WithTimestamp<T extends new (...args: any[]) => object>(Base: T) {
return class extends Base {
createdAt = new Date();
};
}

class BaseUser {
name = 'Alice';
}

const User = WithTimestamp(BaseUser);
const user2 = new User();
console.log(user2.createdAt); // 类型正确

Q5: TC39 Stage 3 装饰器和 TypeScript 实验性装饰器有什么区别?

答案

TC39 Stage 3 装饰器(TypeScript 5.0+ 原生支持)和 TypeScript 实验性装饰器(experimentalDecorators)是两套完全不兼容的方案,核心区别如下:

特性实验性装饰器TC39 标准装饰器
配置需要 experimentalDecorators: true无需配置,TS 5.0+ 默认支持
方法装饰器参数(target, key, descriptor) 三个参数(target, context) 两个参数
context 对象包含 namekindaddInitializer
参数装饰器支持不支持
accessor 关键字不支持支持(自动生成 getter/setter)
元数据emitDecoratorMetadata + reflect-metadata原生 Symbol.metadata(提案中)
执行时机类定义时类定义时,但 addInitializer 可注册延迟执行
互相兼容-不兼容,不能混用

方法装饰器对比

// ====== 实验性装饰器 ======
// tsconfig: "experimentalDecorators": true
function OldLog(
target: any, // 类的原型对象
propertyKey: string, // 方法名
descriptor: PropertyDescriptor // 属性描述符
): PropertyDescriptor {
const original = descriptor.value;
descriptor.value = function (...args: any[]) {
console.log(`[old] calling ${propertyKey}`);
return original.apply(this, args);
};
return descriptor;
}

// ====== TC39 标准装饰器 ======
// 无需额外配置
function NewLog<T extends (...args: any[]) => any>(
target: T, // 方法本身
context: ClassMethodDecoratorContext // 上下文对象
): T {
const methodName = String(context.name);

function replacement(this: any, ...args: any[]): any {
console.log(`[new] calling ${methodName}`);
return target.call(this, ...args);
}

return replacement as T;
}

addInitializer —— 标准装饰器的新能力

function Bound<T extends (...args: any[]) => any>(
target: T,
context: ClassMethodDecoratorContext
): void {
const methodName = context.name;

// addInitializer 在类实例化时执行
context.addInitializer(function (this: any) {
// 自动绑定 this
this[methodName] = this[methodName].bind(this);
});
}

class Button {
label = 'Click me';

@Bound
handleClick() {
console.log(this.label); // 无论怎么调用,this 都正确
}
}

const btn = new Button();
const handler = btn.handleClick;
handler(); // 'Click me'(不会丢失 this)

元数据对比

// 实验性装饰器:需要 reflect-metadata 库
import 'reflect-metadata';

function Type(target: any, key: string) {
const type = Reflect.getMetadata('design:type', target, key);
console.log(`${key} type: ${type.name}`);
}

// TC39 标准装饰器:使用原生 Symbol.metadata(提案阶段)
function Meta(key: string, value: unknown) {
return function (
target: any,
context: ClassFieldDecoratorContext
) {
// 通过 context.metadata 存储元数据
context.metadata[key] = value;
};
}

class User {
@Meta('label', '用户名')
name = '';
}

// 读取元数据
console.log(User[Symbol.metadata]); // { label: '用户名' }
注意
  • 如果项目使用 Angular / NestJS,目前仍需开启 experimentalDecorators,因为这些框架尚未完全迁移到标准装饰器
  • 新项目建议使用 TC39 标准装饰器
  • 两种装饰器语法不能混用,开启 experimentalDecorators 后 TS 会按旧语义解析所有装饰器

Q6: 如何用装饰器实现一个方法缓存(Memoize)?

答案

方法缓存装饰器通过拦截方法调用、用参数序列化为 key 存储结果,下次相同参数直接返回缓存。进阶版还可以支持 TTL 过期策略。

基础版(实验性装饰器)

function Memoize(
target: any,
propertyKey: string,
descriptor: PropertyDescriptor
): PropertyDescriptor {
const originalMethod = descriptor.value;
// 使用 WeakMap 绑定实例,避免不同实例共享缓存
const instanceCache = new WeakMap<object, Map<string, unknown>>();

descriptor.value = function (this: object, ...args: unknown[]) {
if (!instanceCache.has(this)) {
instanceCache.set(this, new Map());
}
const cache = instanceCache.get(this)!;

// 参数序列化为 key
const key = JSON.stringify(args);

if (cache.has(key)) {
return cache.get(key);
}

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

return descriptor;
}

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

const math = new MathService();
console.log(math.fibonacci(40)); // 很快,因为有缓存

带 TTL 过期策略版本

interface CacheEntry<T> {
value: T;
expireAt: number;
}

function MemoizeWithTTL(ttlMs: number = 60_000) {
return function (
target: any,
propertyKey: string,
descriptor: PropertyDescriptor
): PropertyDescriptor {
const originalMethod = descriptor.value;
const instanceCache = new WeakMap<
object,
Map<string, CacheEntry<unknown>>
>();

descriptor.value = function (this: object, ...args: unknown[]) {
if (!instanceCache.has(this)) {
instanceCache.set(this, new Map());
}
const cache = instanceCache.get(this)!;
const key = JSON.stringify(args);
const now = Date.now();

// 检查缓存是否存在且未过期
if (cache.has(key)) {
const entry = cache.get(key)!;
if (now < entry.expireAt) {
return entry.value;
}
// 过期则删除
cache.delete(key);
}

const result = originalMethod.apply(this, args);
cache.set(key, { value: result, expireAt: now + ttlMs });
return result;
};

return descriptor;
};
}

class DataService {
@MemoizeWithTTL(5000) // 缓存 5 秒
fetchConfig(key: string): string {
console.log('expensive computation for', key);
return `config_${key}_${Date.now()}`;
}
}

const svc = new DataService();
svc.fetchConfig('theme'); // expensive computation for theme
svc.fetchConfig('theme'); // 命中缓存,不输出
// 5 秒后...
svc.fetchConfig('theme'); // expensive computation for theme(缓存已过期)

TC39 标准装饰器版本

function MemoizeNew<T extends (...args: any[]) => any>(
target: T,
context: ClassMethodDecoratorContext
): T {
const methodName = String(context.name);
const instanceCache = new WeakMap<object, Map<string, unknown>>();

function memoized(this: object, ...args: any[]): any {
if (!instanceCache.has(this)) {
instanceCache.set(this, new Map());
}
const cache = instanceCache.get(this)!;
const key = JSON.stringify(args);

if (cache.has(key)) {
return cache.get(key);
}

const result = target.call(this, ...args);
cache.set(key, result);
return result;
}

return memoized as T;
}

class Calculator {
@MemoizeNew
expensiveCalc(n: number): number {
console.log('computing...');
return n * n;
}
}

支持异步方法的 Memoize

function MemoizeAsync(
target: any,
propertyKey: string,
descriptor: PropertyDescriptor
): PropertyDescriptor {
const originalMethod = descriptor.value;
const instanceCache = new WeakMap<object, Map<string, Promise<unknown>>>();

descriptor.value = function (this: object, ...args: unknown[]) {
if (!instanceCache.has(this)) {
instanceCache.set(this, new Map());
}
const cache = instanceCache.get(this)!;
const key = JSON.stringify(args);

if (cache.has(key)) {
return cache.get(key);
}

// 缓存 Promise 本身,实现请求去重
const promise = originalMethod.apply(this, args);

// 失败时清除缓存,允许重试
const cachedPromise = (promise as Promise<unknown>).catch(
(err: unknown) => {
cache.delete(key);
throw err;
}
);

cache.set(key, cachedPromise);
return cachedPromise;
};

return descriptor;
}

class ApiService {
@MemoizeAsync
async getUser(id: number): Promise<{ name: string }> {
console.log('fetching user', id);
const res = await fetch(`/api/users/${id}`);
return res.json();
}
}

const api = new ApiService();
// 同时发起两次相同请求,实际只发一次网络请求
const [user1, user2] = await Promise.all([
api.getUser(1),
api.getUser(1),
]);
要点
  • 使用 WeakMap<object, Map> 将缓存绑定到实例,不同实例不共享,实例销毁时缓存自动回收
  • JSON.stringify(args) 做参数序列化,适用于基本类型参数;复杂对象需自定义 key 生成器
  • 异步版本缓存 Promise 本身,可实现请求去重(多次相同调用只触发一次实际请求)
  • 失败时清除缓存,避免缓存错误结果

相关链接