Що таке декоратори в TypeScript?
Декоратори TypeScript - це функції, що застосовуються до класів, методів, властивостей або параметрів через синтаксис @decorator і виконуються в момент визначення класу або члена, а не при виклику.
Теорія
TL;DR
- Декоратор - як штамп на конверті: лист всередині не змінюється, але штамп впливає на те, як його обробляють.
- Компілятор перетворює
@Logger class User {}наLogger(User)ще до запуску коду. - Потрібен
"experimentalDecorators": trueуtsconfig.json. - Використовуй для повторюваної логіки: логування, валідація, метадані маршрутів.
- Для разової логіки проста функція-обгортка простіша і краще тестується.
Швидкий приклад
// tsconfig.json: "experimentalDecorators": true
function LogCreation(target: any) {
console.log(`Клас визначено: ${target.name}`);
}
@LogCreation
class User {
name = "Alice";
}
// Виводить: "Клас визначено: User"
// Спрацьовує при завантаженні файлу, не при new User()Коли я вперше побачив це в NestJS-проекті, поведінка «виконується при завантаженні модуля, а не при new» збила з пантелику. Зараз бачу те саме у всіх, хто зустрічає декоратори вперше.
Як компілятор обробляє декоратори
TypeScript сканує @decorator під час emit-фази і переписує його на звичайний виклик функції. @Logger class User {} стає приблизно User = Logger(User). Ніякого спеціального рантайму - тільки трансформований AST.
Якщо увімкнути "emitDecoratorMetadata": true, компілятор додає символи Reflect.metadata через tslib. Саме так Angular та NestJS інжектують типи залежностей без явної передачі.
TypeScript 5.0+ має два режими: legacy (experimentalDecorators) і новий режим за пропозалом TC39 stage 3. Більшість фреймворків, зокрема Angular і NestJS, поки що використовують legacy.
Коли використовувати
- Повторюваний шаблонний код у багатьох класах: логування, вимірювання часу, обробка помилок. Декоратор тримає все DRY.
- Контракти фреймворку:
@Componentв Angular,@Getв NestJS. Це не опція. - Валідація DTO через class-validator:
@IsEmail(),@IsString(). - Якщо логіка зустрічається один раз - проста функція зрозуміліша і краще тестується.
- Уникай на гарячих шляхах виконання: кожен декоратор додає шар виклику функції.
Типові помилки
Забули experimentalDecorators у tsconfig:
@Log class User {} // Error: Decorators not enabledДодай "experimentalDecorators": true у tsconfig.json, або передай --experimentalDecorators у CLI.
Не повертають descriptor у декораторі методу:
function BadLog(target: any, key: string, desc: PropertyDescriptor) {
desc.value = () => console.log("called"); // Модифікує, але не повертає
}Бандлери типу esbuild очікують повернення зміненого PropertyDescriptor. Завжди return desc.
Декоратор на стрілкову функцію-поле класу:
class SearchService {
@Throttle(1000)
search = () => { /* ... */ }; // Throttle перевіряє desc.value - там undefined
}Стрілкові функції як поля класу не мають value у дескрипторі так само, як звичайні методи. Декоратор виконується, але нічого не робить. Використовуй звичайні методи.
Reflect.getMetadata без обох прапорців:
const type = Reflect.getMetadata("design:type", target, key); // undefinedПотрібні "emitDecoratorMetadata": true у tsconfig І import "reflect-metadata" у точці входу. Якщо одного немає - undefined скрізь.
Декоратор на приватні поля через #:
class C {
@Log #private = 1; // Немає runtime descriptor для private fields
}Приватні поля через # не мають доступного PropertyDescriptor під час виконання. Використовуй private від TypeScript, якщо властивість має декоруватись.
Де зустрічається
- NestJS:
@Controller('/users'),@Get(':id'),@Body()для маршрутизації і вилучення параметрів. - Angular:
@Component,@Injectable,@Inputдля метаданих компонентів і DI. - TypeORM:
@Entity(),@Column(),@PrimaryGeneratedColumn()для схеми бази даних. - class-validator:
@IsEmail(),@MinLength(8)для валідації DTO. - class-transformer:
@Expose(),@Transform()для контролю серіалізації.
Питання на співбесіді
Q: Які аргументи отримує декоратор методу?
A: Три: target (прототип класу для instance-методів, конструктор для статичних), propertyKey (ім'я методу як рядок або symbol), descriptor (об'єкт PropertyDescriptor з полями value, writable, enumerable, configurable).
Q: В якому порядку виконуються стаковані декоратори?
A: Фабрики декораторів запускаються зверху вниз. Самі декоратори застосовуються знизу вгору. Тобто @A @B method() - спочатку викликаються A() і B() як фабрики, але застосовується спочатку B, а потім A огортає результат.
Q: Чи можна отримати параметри конструктора з декоратора класу?
A: Ні. Декоратор класу отримує тільки функцію-конструктор. Щоб перехопити параметри конструктора, потрібно повернути новий клас, що розширює оригінальний і перевизначає конструктор.
Q: Як декоратори взаємодіють зі спадкуванням?
A: Декоратори методів дочірнього класу модифікують прототип дочірнього, а не батьківського. Якщо батько має декорований метод, а дитина перевизначає його без декоратора - метод дитини не декорований.
Q: Навіщо фабрики декораторів типу @Throttle(1000) замість простого @Throttle?
A: Фабрика - це функція, що повертає справжній декоратор. Патерн дозволяє передавати конфігурацію для кожного використання. Без нього параметризувати поведінку, наприклад затримку або рівень логу, не вийде.
Приклади
Базовий: декоратор класу для логування
function LogCreation(constructor: Function) {
console.log(`Клас визначено: ${constructor.name}`);
}
@LogCreation
class UserService {
constructor(private name: string) {}
greet() {
return `Hello, ${this.name}`;
}
}
// Виводить: "Клас визначено: UserService" - при завантаженні модуля
const svc = new UserService("Alice"); // Нічого додаткового не логується
console.log(svc.greet()); // "Hello, Alice"Декоратор запускається один раз при парсингу модуля. Не при кожному new UserService(). Це та точка, де більшість розробників коригують свою ментальну модель.
Середній: фабрика декораторів з throttle
function Throttle(ms: number) {
return function (target: any, key: string, desc: PropertyDescriptor) {
if (!desc.value || typeof desc.value !== "function") return desc;
let lastCall = 0;
const original = desc.value;
desc.value = function (...args: any[]) {
const now = Date.now();
if (now - lastCall < ms) return;
lastCall = now;
return original.apply(this, args);
};
return desc;
};
}
class SearchService {
@Throttle(1000)
search(query: string) {
console.log(`Пошук: ${query}`);
// В продакшені: fetch(`/api/search?q=${query}`)
}
}
const service = new SearchService();
service.search("typescript"); // Виводить: "Пошук: typescript"
service.search("decorators"); // Ігнорується - пройшло менше 1000мсПатерн фабрики (Throttle(1000)) дозволяє налаштовувати затримку для кожного методу окремо. return desc обов'язковий: бандлери очікують повернення дескриптора, і пропуск дає нестабільну поведінку залежно від тулчейну.
Просунутий: збір метаданих маршрутів (патерн NestJS)
import "reflect-metadata";
function Get(path: string) {
return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) {
const routes = Reflect.getMetadata("routes", target.constructor) || [];
routes.push({ path, method: "GET", handler: propertyKey });
Reflect.defineMetadata("routes", routes, target.constructor);
return descriptor;
};
}
class UserController {
@Get("/users")
getUsers() {
return [{ id: 1, name: "Alice" }];
}
@Get("/users/:id")
getUserById() {
return { id: 1, name: "Alice" };
}
}
// Читаємо метадані для реєстрації маршрутів:
const routes = Reflect.getMetadata("routes", UserController);
console.log(routes);
// [
// { path: '/users', method: 'GET', handler: 'getUsers' },
// { path: '/users/:id', method: 'GET', handler: 'getUserById' }
// ]Це реальний патерн, який NestJS використовує всередині. Декоратори зберігають метадані на класі, а бутстрапер зчитує їх пізніше для реєстрації маршрутів в Express. Сам декоратор не торкається Express взагалі. Це розділення і робить патерн компонованим.
Коротка відповідь
Для співбесідиКоротка відповідь допоможе вам впевнено відповідати на цю тему під час співбесіди.