Skip to main content

Що таке декоратори в TypeScript?

Декоратори TypeScript - це функції, що застосовуються до класів, методів, властивостей або параметрів через синтаксис @decorator і виконуються в момент визначення класу або члена, а не при виклику.

Теорія

TL;DR

  • Декоратор - як штамп на конверті: лист всередині не змінюється, але штамп впливає на те, як його обробляють.
  • Компілятор перетворює @Logger class User {} на Logger(User) ще до запуску коду.
  • Потрібен "experimentalDecorators": true у tsconfig.json.
  • Використовуй для повторюваної логіки: логування, валідація, метадані маршрутів.
  • Для разової логіки проста функція-обгортка простіша і краще тестується.

Швидкий приклад

typescript
// 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:

typescript
@Log class User {} // Error: Decorators not enabled

Додай "experimentalDecorators": true у tsconfig.json, або передай --experimentalDecorators у CLI.

Не повертають descriptor у декораторі методу:

typescript
function BadLog(target: any, key: string, desc: PropertyDescriptor) { desc.value = () => console.log("called"); // Модифікує, але не повертає }

Бандлери типу esbuild очікують повернення зміненого PropertyDescriptor. Завжди return desc.

Декоратор на стрілкову функцію-поле класу:

typescript
class SearchService { @Throttle(1000) search = () => { /* ... */ }; // Throttle перевіряє desc.value - там undefined }

Стрілкові функції як поля класу не мають value у дескрипторі так само, як звичайні методи. Декоратор виконується, але нічого не робить. Використовуй звичайні методи.

Reflect.getMetadata без обох прапорців:

typescript
const type = Reflect.getMetadata("design:type", target, key); // undefined

Потрібні "emitDecoratorMetadata": true у tsconfig І import "reflect-metadata" у точці входу. Якщо одного немає - undefined скрізь.

Декоратор на приватні поля через #:

typescript
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: Фабрика - це функція, що повертає справжній декоратор. Патерн дозволяє передавати конфігурацію для кожного використання. Без нього параметризувати поведінку, наприклад затримку або рівень логу, не вийде.

Приклади

Базовий: декоратор класу для логування

typescript
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

typescript
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)

typescript
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 взагалі. Це розділення і робить патерн компонованим.

Коротка відповідь

Для співбесіди
Premium

Коротка відповідь допоможе вам впевнено відповідати на цю тему під час співбесіди.

Дочитали статтю?