← Курс/Декораторы в TypeScript#164 из 257+35 XP

Декораторы в TypeScript

Что такое декоратор

Декоратор — это специальная функция, которая добавляет поведение классу, методу или свойству без изменения их исходного кода. Декораторы включаются флагом experimentalDecorators: true в tsconfig.json.

// tsconfig.json
{
  "compilerOptions": {
    "experimentalDecorators": true,
    "emitDecoratorMetadata": true  // для работы с metadata
  }
}

Декоратор класса

Принимает конструктор класса, может его модифицировать или заменить:

function Sealed(constructor: Function) {
  Object.seal(constructor)           // нельзя добавить статические свойства
  Object.seal(constructor.prototype) // нельзя добавить методы в прототип
}

function AddTimestamp<T extends { new(...args: any[]): {} }>(Base: T) {
  return class extends Base {
    createdAt = new Date()
  }
}

@Sealed
@AddTimestamp
class User {
  constructor(public name: string) {}
}

const u = new User('Алексей')
console.log((u as any).createdAt)  // Date добавлена декоратором

Декоратор метода

Принимает три аргумента: прототип, имя метода, дескриптор:

function Log(target: any, name: string, descriptor: PropertyDescriptor) {
  const original = descriptor.value
  descriptor.value = function (...args: any[]) {
    console.log(`Вызов ${name}(${args.join(', ')})`)
    const result = original.apply(this, args)
    console.log(`Результат: ${result}`)
    return result
  }
  return descriptor
}

function Memoize(target: any, name: string, descriptor: PropertyDescriptor) {
  const original = descriptor.value
  const cache = new Map()
  descriptor.value = function (...args: any[]) {
    const key = JSON.stringify(args)
    if (cache.has(key)) return cache.get(key)
    const result = original.apply(this, args)
    cache.set(key, result)
    return result
  }
}

class Calculator {
  @Log
  add(a: number, b: number): number {
    return a + b
  }

  @Memoize
  fibonacci(n: number): number {
    if (n <= 1) return n
    return this.fibonacci(n - 1) + this.fibonacci(n - 2)
  }
}

Декоратор свойства

function Required(target: any, key: string) {
  let value = target[key]

  Object.defineProperty(target, key, {
    get() { return value },
    set(newVal) {
      if (newVal == null || newVal === '') {
        throw new Error(`Поле ${key} обязательно`)
      }
      value = newVal
    }
  })
}

class Form {
  @Required
  name: string = ''

  @Required
  email: string = ''
}

Фабрика декораторов

Для передачи параметров декоратор оборачивают в функцию:

function Throttle(ms: number) {
  return function(target: any, name: string, descriptor: PropertyDescriptor) {
    const original = descriptor.value
    let lastCall = 0
    descriptor.value = function (...args: any[]) {
      const now = Date.now()
      if (now - lastCall >= ms) {
        lastCall = now
        return original.apply(this, args)
      }
    }
    return descriptor
  }
}

class SearchInput {
  @Throttle(300)
  search(query: string) {
    console.log('Поиск:', query)
  }
}

Реальное применение

Декораторы активно используются во фреймворках:

// NestJS — веб-фреймворк
@Controller('/users')
class UserController {
  @Get('/:id')
  @UseGuards(AuthGuard)
  async getUser(@Param('id') id: string) { ... }
}

// TypeORM — ORM для баз данных
@Entity()
class User {
  @PrimaryGeneratedColumn()
  id: number

  @Column({ unique: true })
  email: string
}

Примеры

Декораторы через обычные функции-обёртки: memoize, throttle, readonly, log — те же паттерны, что и TS-декораторы

// Декораторы в TypeScript компилируются в вызовы функций.
// Покажем те же паттерны через обычные функции в JS.

// --- Декоратор метода: memoize ---
function memoize(fn) {
  const cache = new Map()
  return function (...args) {
    const key = JSON.stringify(args)
    if (cache.has(key)) {
      console.log(`[memoize] кэш: ${key}`)
      return cache.get(key)
    }
    const result = fn.apply(this, args)
    cache.set(key, result)
    return result
  }
}

// --- Декоратор метода: log ---
function log(fn, name = fn.name) {
  return function (...args) {
    console.log(`[log] ${name}(${args.join(', ')})`)
    const result = fn.apply(this, args)
    console.log(`[log] ${name}${result}`)
    return result
  }
}

// --- Декоратор метода: throttle ---
function throttle(fn, ms) {
  let lastCall = 0
  return function (...args) {
    const now = Date.now()
    if (now - lastCall >= ms) {
      lastCall = now
      return fn.apply(this, args)
    } else {
      console.log(`[throttle] пропущен: прошло ${now - lastCall}мс из ${ms}мс`)
    }
  }
}

// --- Декоратор метода: retry ---
function retry(fn, attempts = 3) {
  return async function (...args) {
    for (let i = 0; i < attempts; i++) {
      try {
        return await fn.apply(this, args)
      } catch (e) {
        console.log(`[retry] попытка ${i + 1} не удалась: ${e.message}`)
        if (i === attempts - 1) throw e
      }
    }
  }
}

// --- Декоратор класса: sealed (запрет изменения прототипа) ---
function sealed(Class) {
  Object.seal(Class)
  Object.seal(Class.prototype)
  return Class
}

// --- Применение ---

class Calculator {
  fibonacci(n) {
    if (n <= 1) return n
    return this.fibonacci(n - 1) + this.fibonacci(n - 2)
  }

  add(a, b) { return a + b }
}

const calc = new Calculator()

// Применяем memoize + log вручную (как decorator)
calc.fibonacci = memoize(calc.fibonacci.bind(calc))

console.log('=== Memoize: fibonacci ===')
console.log(calc.fibonacci(10))   // вычисляет
console.log(calc.fibonacci(10))   // из кэша

const loggedAdd = log(calc.add.bind(calc), 'add')
console.log('\n=== Log: add ===')
loggedAdd(3, 5)
loggedAdd(10, 20)

console.log('\n=== Throttle: search ===')
const search = throttle((query) => {
  console.log('Поиск:', query)
}, 200)

search('TypeScript')   // выполнится
search('декораторы')   // пропустится (< 200мс)

setTimeout(() => {
  search('JavaScript') // выполнится (>= 200мс)
}, 250)

console.log('\n=== Retry: fetch-like ===')
let attempts = 0
const unreliable = retry(async () => {
  attempts++
  if (attempts < 3) throw new Error('Сеть недоступна')
  return 'Данные получены'
}, 3)

unreliable().then(result => {
  console.log('Результат:', result)
  console.log('Попыток потребовалось:', attempts)
})