13 / 37
Как типизировать decorator в TypeScript 5?
Полный ответ
Декоратор метода в TC39 Stage 3 получает два аргумента: оригинальную функцию и контекст:
TypeScript
// Декоратор метода — логирование вызовов
function log<This, Args extends unknown[], Return>(
originalMethod: (this: This, ...args: Args) => Return,
context: ClassMethodDecoratorContext<This, (this: This, ...args: Args) => Return>
) {
const methodName = String(context.name);
function replacement(this: This, ...args: Args): Return {
console.log(`Calling ${methodName} with`, args);
const result = originalMethod.call(this, ...args);
console.log(`${methodName} returned`, result);
return result;
}
return replacement;
}
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]"
// "add returned 5"
Декоратор поля (field decorator) не получает значение — он возвращает инициализатор:
TypeScript
// Декоратор поля — валидация при присвоении
function min(minValue: number) {
return function <This>(
_value: undefined,
context: ClassFieldDecoratorContext<This, number>
) {
return function (this: This, initialValue: number): number {
if (initialValue < minValue) {
throw new Error(`${String(context.name)} must be >= ${minValue}, got ${initialValue}`);
}
return initialValue;
};
};
}
class Product {
@min(0)
price: number;
constructor(price: number) {
this.price = price;
}
}
new Product(10); // OK
// new Product(-5); // Error: price must be >= 0, got -5
Декоратор accessor — работает с парой get/set:
TypeScript
// auto-accessor + декоратор
function clamp(low: number, high: number) {
return function <This>(
target: ClassAccessorDecoratorTarget<This, number>,
context: ClassAccessorDecoratorContext<This, number>
): ClassAccessorDecoratorResult<This, number> {
return {
get(this: This): number {
return target.get.call(this);
},
set(this: This, value: number) {
const clamped = Math.max(low, Math.min(high, value));
target.set.call(this, clamped);
},
init(value: number): number {
return Math.max(low, Math.min(high, value));
},
};
};
}
class Slider {
@clamp(0, 100)
accessor value = 50;
}
const slider = new Slider();
slider.value = 200;
console.log(slider.value); // 100
Декоратор класса:
TypeScript
// Декоратор класса — добавить toString
function serializable<T extends new (...args: unknown[]) => object>(
Base: T,
_context: ClassDecoratorContext<T>
) {
return class extends Base {
toString() {
return JSON.stringify(this);
}
};
}
@serializable
class Config {
constructor(
public host: string,
public port: number
) {}
}
console.log(new Config("localhost", 3000).toString());
// '{"host":"localhost","port":3000}'
Реальные кейсы
Замер производительности метода:
TypeScript
function measure<This, Args extends unknown[], Return>(
originalMethod: (this: This, ...args: Args) => Return,
context: ClassMethodDecoratorContext
) {
const name = String(context.name);
return function (this: This, ...args: Args): Return {
const start = performance.now();
const result = originalMethod.call(this, ...args);
console.log(`${name}: ${(performance.now() - start).toFixed(2)}ms`);
return result;
};
}
Ошибка: путать Stage 3 и legacy декораторы:
TypeScript
// ❌ Legacy (experimentalDecorators) — другая сигнатура
function oldDecorator(target: object, key: string, descriptor: PropertyDescriptor) {}
// ✅ Stage 3 (TypeScript 5+) — context-объект
function newDecorator(method: Function, context: ClassMethodDecoratorContext) {}
Резюме
Декораторы Stage 3 в TypeScript 5 типизируются через контекстные интерфейсы: ClassMethodDecoratorContext, ClassFieldDecoratorContext, ClassAccessorDecoratorContext, ClassDecoratorContext. Каждый получает оригинальное значение и context с метаданными. Не нужно включать experimentalDecorators — это новый стандарт.