Декораторы в TypeScript
Декораторы — механизм метапрограммирования, позволяющий модифицировать поведение классов и их элементов через аннотации. Они не добавляют новой функциональности — они оборачивают существующую, добавляя поведение на уровне определения.
Понимание декораторов помогает решать задачи сквозной логики — логирования, валидации, кэширования — без дублирования кода. В этой статье разберём две спецификации (Legacy и Stage 3), рассмотрим все виды декораторов и научимся применять их в реальных фреймворках.
Содержание
- Архитектурная идея
- Когда вызываются декораторы
- Две спецификации: Legacy и Stage 3
- Legacy-декораторы (experimentalDecorators)
- Stage 3 Декораторы (TC39)
- Где используются декораторы
- Когда НЕ использовать декораторы
- Типичные ошибки
- Ключевые тезисы для интервью
- Заключение
- Полезные ссылки
Архитектурная идея
Декораторы решают проблему сквозной логики (cross-cutting concerns). Логирование, валидация, кэширование, авторизация — это поведение, которое нужно во многих местах, но не является частью бизнес-логики.
Без декораторов вы дублируете эту логику:
class UserService {
async getUser(id: string) {
console.log("getUser called with", id);
const start = Date.now();
try {
const user = await db.users.findById(id);
return user;
} finally {
console.log("getUser took", Date.now() - start, "ms");
}
}
async updateUser(id: string, data: Partial<User>) {
console.log("updateUser called with", id);
const start = Date.now();
try {
await db.users.update(id, data);
} finally {
console.log("updateUser took", Date.now() - start, "ms");
}
}
}
С декораторами — определяете поведение один раз:
class UserService {
@log @measure
async getUser(id: string) {
return db.users.findById(id);
}
@log @measure
async updateUser(id: string, data: Partial<User>) {
await db.users.update(id, data);
}
}
Это Aspect-Oriented Programming (AOP) — парадигма, в которой сквозная логика выделяется в отдельные модули и применяется к коду декларативно.
Когда вызываются декораторы
Критически важно: декораторы выполняются при определении класса (когда модуль загружается), а не при создании экземпляра.
function logClass(target: Function) {
console.log("Декоратор вызван для:", target.name);
}
@logClass
class User {
constructor(public name: string) {}
}
// При загрузке модуля выведет: "Декоратор вызван для: User"
// При new User() — НЕ выведет ничего
Это означает:
- Декораторы не имеют доступа к
thisэкземпляра (его ещё не существует) - Декораторы выполняются один раз за всё время жизни приложения
- Порядок применения — снизу вверх (от класса к методу)
Две спецификации: Legacy и Stage 3
TypeScript поддерживает две версии декораторов, и это не просто разные синтаксисы — это принципиально разные API.
Legacy (experimentalDecorators) | Stage 3 (TC39 стандарт) | |
|---|---|---|
| Сигнатура | (target, key, descriptor) | (value, context) |
| Поддержка TS | experimentalDecorators: true | По умолчанию (TS 5.0+) |
| Статус | Экспериментальная | Стандарт ECMAScript |
| Доступ к метаданным | Через reflect-metadata | Через context.metadata |
this в декораторе | Не доступен | Через context.access |
Legacy-декораторы — это экспериментальная фича TypeScript, которая предшествовала стандарту. Stage 3 — финальная спецификация, принятая TC39.
Legacy-декораторы (experimentalDecorators)
Для использования нужна настройка в tsconfig.json:
{
"compilerOptions": {
"experimentalDecorators": true,
"emitDecoratorMetadata": true
}
}
Декораторы классов
Получают конструктор класса. Могут модифицировать или заменить его:
function sealed(constructor: Function) {
Object.seal(constructor);
Object.seal(constructor.prototype);
}
@sealed
class User {
constructor(public name: string) {}
}
Декоратор с параметрами — это функция, возвращающая декоратор:
function singleton(scope: "global" | "request" = "global") {
return function <T extends { new(...args: any[]): {} }>(constructor: T) {
let instance: any;
return class extends constructor {
constructor(...args: any[]) {
if (!instance) instance = new constructor(...args);
return instance;
}
};
};
}
@singleton("global")
class Database {
constructor(public connectionString: string) {}
}
Декораторы методов
Получают три аргумента:
target— прототип класса (дляstatic— сам класс)key— имя методаdescriptor— дескриптор свойства, который можно модифицировать
function log(
target: any,
key: string,
descriptor: PropertyDescriptor
): PropertyDescriptor {
const original = descriptor.value;
descriptor.value = function (...args: any[]) {
console.log(`Calling ${key} with`, args);
const result = original.apply(this, args);
console.log(`${key} returned`, result);
return result;
};
return descriptor;
}
class Calculator {
@log
add(a: number, b: number): number {
return a + b;
}
}
Декораторы свойств
Получают target (прототип) и key (имя свойства). Не получают дескриптор — потому что свойство ещё не инициализировано на момент вызова декоратора. Могут только добавить метаданные или определить getter/setter:
function readonly(target: any, key: string) {
Object.defineProperty(target, key, {
writable: false,
configurable: false
});
}
class Config {
@readonly
apiUrl = "https://api.example.com";
}
Декораторы параметров
Не могут модифицировать значение — только добавлять метаданные:
function logParam(target: any, key: string, index: number) {
console.log(`Param ${index} in ${key}`);
}
class UserService {
createUser(@logParam name: string, @logParam age: number) {
// ...
}
}
Декораторы accessor (get/set)
Работают как декораторы методов — получают дескриптор:
function uppercase(target: any, key: string, descriptor: PropertyDescriptor) {
const original = descriptor.get!;
descriptor.get = function () {
const value = original.call(this);
return typeof value === "string" ? value.toUpperCase() : value;
};
return descriptor;
}
class User {
private _name = "john";
@uppercase
get name() { return this._name; }
}
new User().name; // "JOHN"
Stage 3 Декораторы (TC39)
Новая спецификация, принятая как стандарт ECMAScript. TypeScript поддерживает её начиная с 5.0 без дополнительных флагов.
Принципиально другой API
Вместо (target, key, descriptor) — (value, context):
value— декорируемый элемент (функция метода, класс, initializer)context— объект с информацией о контексте:context.kind— тип элемента:"class","method","getter","setter","field","accessor"context.name— имя элемента (строка, символ или вычисленное)context.access— объект для доступа к элементу:access.get(obj),access.set(obj, value)context.metadata— хранилище метаданных (заменаreflect-metadata)
Декоратор метода
function logged(value: Function, context: ClassMethodDecoratorContext) {
return function (this: any, ...args: any[]) {
console.log(`Calling ${String(context.name)}`);
return value.apply(this, args);
};
}
class User {
@logged
greet() {
return "Hello";
}
}
Декоратор класса
function registered(value: typeof User, context: ClassDecoratorContext) {
return class extends value {
constructor(...args: any[]) {
super(...args);
console.log(`Instance of ${String(context.name)} created`);
}
};
}
@registered
class User {
constructor(public name: string) {}
}
Декоратор поля (field)
В Stage 3 декораторы полей работают через initializer — функцию, которая вызывается при инициализации поля:
function observed(initialValue: any, context: ClassFieldDecoratorContext) {
const { name } = context;
return function (this: any) {
const stored = initialValue;
Object.defineProperty(this, name, {
get() { return stored; },
set(v: any) {
console.log(`${String(name)} changed to`, v);
}
});
return stored;
};
}
class User {
@observed
name = "John";
}
Автоматическая установка доступа (auto-accessor)
Stage 3 вводит новый синтаксис accessor, который автоматически создаёт getter/setter:
function logged(value: { get(): any; set(v: any): void }, context: ClassAccessorDecoratorContext) {
return {
get(this: any) {
console.log(`Reading ${String(context.name)}`);
return value.get.call(this);
},
set(this: any, newValue: any) {
console.log(`Setting ${String(context.name)} to`, newValue);
value.set.call(this, newValue);
}
};
}
class User {
@logged
accessor name = "John";
}
Метаданные без reflect-metadata
Stage 3 декораторы имеют встроенное хранилище метаданных:
const ROUTES = Symbol("routes");
function Route(path: string) {
return function (value: Function, context: ClassMethodDecoratorContext) {
context.metadata[ROUTES] ??= [];
context.metadata[ROUTES].push({ method: context.name, path });
};
}
class UserController {
@Route("/users")
getAll() { return []; }
@Route("/users/:id")
getById() { return null; }
}
Где используются декораторы
NestJS — декораторы как основа фреймворка
NestJS построен на декораторах для DI, маршрутизации и валидации:
@Controller("users")
class UserController {
constructor(private userService: UserService) {}
@Get()
findAll(): Promise<User[]> {
return this.userService.findAll();
}
@Post()
create(@Body() dto: CreateUserDto): Promise<User> {
return this.userService.create(dto);
}
}
@Injectable()
class UserService {
constructor(@Inject("USER_REPOSITORY") private repo: Repository<User>) {}
}
class CreateUserDto {
@IsString() @MinLength(2) @MaxLength(50)
name: string;
@IsEmail()
email: string;
}
MobX — реактивность через декораторы
class UserStore {
@observable users: User[] = [];
@observable loading = false;
@computed get activeUsers() {
return this.users.filter(u => u.isActive);
}
@action addUser(user: User) {
this.users.push(user);
}
}
Vue — class-based компоненты (legacy)
Vue 2 с vue-property-decorator и Vue 3 с vue-facing-decorator:
@Component
class UserCard extends Vue {
@Prop({ required: true }) userId!: number;
@Watch("userId", { immediate: true })
async onUserIdChange(id: number) {
this.user = await fetchUser(id);
}
}
Когда НЕ использовать декораторы
-
В функциональном коде. React hooks, Vue composables — декораторы не нужны, если нет классов.
-
Когда достаточно обычных функций или HOC. Декораторы — это синтаксический сахар над обёртками. Если обёртка простая, декоратор избыточен.
-
Если команда не знакома с паттерном. Декораторы — продвинутая фича. Если команда не понимает, как они работают, — лучше использовать явные обёртки.
-
В новом коде на Stage 3. Если проект начинается сейчас, используйте Stage 3 декораторы, а не legacy. Legacy — только для поддержки существующих библиотек (NestJS, MobX).
Типичные ошибки
Забытый experimentalDecorators
// tsconfig.json — без этого legacy-декораторы не работают
{
"compilerOptions": {
"experimentalDecorators": true
}
}
Путаница между legacy и Stage 3
// Legacy — три аргумента
function legacy(target: any, key: string, descriptor: PropertyDescriptor) { }
// Stage 3 — два аргумента
function stage3(value: any, context: ClassMethodDecoratorContext) { }
Декоратор свойства не может изменить значение напрямую
// ❌ Не работает — свойство ещё не инициализировано
function init(target: any, key: string) {
target[key] = "initial";
}
// ✅ Использовать getter/setter
function init(target: any, key: string) {
let value: any;
Object.defineProperty(target, key, {
get() { return value ?? "default"; },
set(v: any) { value = v; }
});
}
Отсутствие reflect-metadata для legacy
// ❌ Reflect.defineMetadata не существует
Reflect.defineMetadata("key", "value", target);
// ✅ Импортировать polyfill
import "reflect-metadata";
Reflect.defineMetadata("key", "value", target);
Ключевые тезисы для интервью
- Декораторы — механизм метапрограммирования для сквозной логики (AOP): логирование, валидация, кэширование.
- Декораторы выполняются при определении класса (загрузке модуля), а не при создании экземпляра.
- TypeScript поддерживает две спецификации: Legacy (
experimentalDecorators) и Stage 3 (TC39 стандарт). - Legacy-декораторы получают
(target, key, descriptor), Stage 3 —(value, context). - Legacy-декораторы бывают пяти видов: классы, методы, свойства, параметры, accessor.
- Stage 3 вводит
context.metadataдля хранения метаданных безreflect-metadata. - Stage 3 добавляет синтаксис
accessorдля автоматического создания getter/setter. - NestJS построен на декораторах: DI, маршрутизация, валидация — всё через аннотации.
- В новом коде предпочтительнее Stage 3 декораторы; Legacy — только для поддержки существующих библиотек.
- Декораторы не нужны в функциональном коде (React hooks, Vue composables) без классов.
Заключение
Декораторы — мощный инструмент для разделения бизнес-логики и сквозной функциональности. Ключевое различие — между Legacy и Stage 3 спецификациями: это принципиально разные API, а не просто синтаксис. Для новых проектов выбирайте Stage 3, для поддержки NestJS и MobX — Legacy. Декораторы не заменяют обычные функции и HOC — применяйте их осознанно, когда паттерн действительно уместен.