TypeScriptSeniorTechnical
Что такое декораторы (decorators) в TypeScript и как они используются?
Декораторы — аннотации для классов, методов, свойств и параметров, изменяющие их поведение или добавляющие метаданные. В TS есть legacy-версия (experimentalDecorators, используется в NestJS/Angular) и новая TC39 stage 3 (TS 5.0+), которые несовместимы между собой.
Декораторы в TypeScript
Декораторы — это специальные объявления, применяемые к классам, методам, свойствам или параметрам для изменения их поведения или добавления метаданных. В TypeScript есть две несовместимых версии декораторов: legacy (experimentalDecorators: true, используется в NestJS/Angular) и TC39 stage 3 (новый стандарт, поддержан с TS 5.0).
Legacy decorators (experimentalDecorators)
// tsconfig.json
// "experimentalDecorators": true,
// "emitDecoratorMetadata": true // для reflect-metadata
// Декоратор класса — принимает конструктор
function Singleton<T extends { new(...args: any[]): {} }>(constructor: T) {
let instance: T | null = null;
return class extends constructor {
constructor(...args: any[]) {
if (instance) return instance as any;
super(...args);
instance = this as any;
}
};
}
@Singleton
class AppConfig {
readonly version = "1.0.0";
}
const a = new AppConfig();
const b = new AppConfig();
console.log(a === b); // true
Декоратор метода
function Log(
target: any,
propertyKey: string,
descriptor: PropertyDescriptor
) {
const original = descriptor.value;
descriptor.value = function (...args: any[]) {
console.log(`Calling ${propertyKey} with`, args);
const result = original.apply(this, args);
console.log(`${propertyKey} returned`, result);
return result;
};
return descriptor;
}
class OrderService {
@Log
createOrder(userId: number, product: string) {
return { userId, product, id: Math.random() };
}
}
Декоратор свойства и параметра
import "reflect-metadata";
const INJECT_KEY = Symbol("inject");
function Inject(token: string) {
return function (target: any, _: string | undefined, index: number) {
const existing = Reflect.getMetadata(INJECT_KEY, target) ?? [];
existing[index] = token;
Reflect.defineMetadata(INJECT_KEY, existing, target);
};
}
function Injectable(target: any) {
// Регистрируем класс в IoC-контейнере
return target;
}
@Injectable
class UserController {
constructor(@Inject("UserService") private userService: any) {}
}
TC39 Stage 3 декораторы (TS 5.0+)
// Без experimentalDecorators, стандартный синтаксис
function readonly<T>(
_target: undefined,
context: ClassFieldDecoratorContext
) {
return function (this: T, value: unknown) {
Object.defineProperty(this, context.name, {
value,
writable: false,
configurable: false,
});
return value;
};
}
class Config {
@readonly
version = "2.0.0";
}
const cfg = new Config();
// cfg.version = "3.0"; // TypeError в runtime
NestJS — практический пример
import { Controller, Get, Param, UseGuards } from "@nestjs/common";
import { AuthGuard } from "./auth.guard";
@Controller("users") // prefix /users
@UseGuards(AuthGuard) // применяется ко всем роутам
export class UsersController {
@Get(":id") // GET /users/:id
async getUser(@Param("id") id: string) {
return { id };
}
}
Подводные камни
- Два несовместимых стандарта: legacy (experimentalDecorators) и TC39 stage 3 декораторы имеют разную сигнатуру и семантику. NestJS и Angular используют legacy — нельзя включать оба режима одновременно.
- emitDecoratorMetadata требует reflect-metadata: без явного импорта
import "reflect-metadata"в точке входа метаданные недоступны в runtime. - Порядок применения: декораторы выполняются снизу вверх (ближайший к объявлению — первым), что контринтуитивно при нескольких декораторах на одном методе.
- Декораторы и наследование: декораторы применяются к конкретному классу, не наследуются автоматически — каждый подкласс должен иметь собственные декораторы.
- Performance overhead: тяжёлые декораторы, выполняющие дорогие операции при инициализации класса, замедляют старт приложения.
- Tree-shaking проблемы: декоратор регистрирует побочные эффекты, которые bundler не может безопасно удалить — это увеличивает bundle size.
- Debugging сложнее: стек вызовов содержит обёртки декораторов, что затрудняет отладку — используйте source maps и понятные имена функций-декораторов.
Common mistakes
- Смешивать «decorators» с похожим механизмом без критерия выбора.
- Игнорировать риск: неверно оценить границы применения темы «decorators» и получить хрупкое решение.
- Показывать только синтаксис и не объяснять поведение в runtime или сборке.
What the interviewer is testing
- Объясняет метаданные и обертки для классов, методов и полей.
- Показывает на примере, как работает: декораторы выполняются на этапе определения класса и могут регистрировать метаданные или менять descriptor; важно различать legacy decorators и современную семантику TypeScript 5.
- Называет production-нюанс и граничный случай для темы «decorators».