Learning Book

Декораторы классов, методов и свойств

Декораторы классов, методов, свойств

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

Получает конструктор класса и может вернуть новый конструктор:

// Декоратор, добавляющий timestamp
function withTimestamp<T extends { new(...args: any[]): {} }>(
  constructor: T
) {
  return class extends constructor {
    createdAt = new Date()
  }
}

@withTimestamp
class Request {
  constructor(public url: string) {}
}

const req = new Request('/api')
console.log((req as any).createdAt) // Date объект

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

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

// Декоратор логирования
function log(
  target: any,
  propertyKey: string,
  descriptor: PropertyDescriptor
): PropertyDescriptor {
  const originalMethod = descriptor.value

  descriptor.value = function (...args: any[]) {
    console.log(`Вызов ${propertyKey}(${JSON.stringify(args)})`)
    const result = originalMethod.apply(this, args)
    console.log(`${propertyKey} вернул: ${JSON.stringify(result)}`)
    return result
  }

  return descriptor
}

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

const calc = new Calculator()
calc.add(2, 3)
// Вызов add([2,3])
// add вернул: 5

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

Получает: прототип и имя свойства (без дескриптора!):

// Декоратор для валидации
function minLength(min: number) {
  return function (target: any, propertyKey: string) {
    let value: string

    const getter = function () {
      return value
    }

    const setter = function (newVal: string) {
      if (newVal.length < min) {
        throw new Error(
          `${propertyKey} должен содержать минимум ${min} символов`
        )
      }
      value = newVal
    }

    Object.defineProperty(target, propertyKey, {
      get: getter,
      set: setter,
      enumerable: true,
      configurable: true,
    })
  }
}

class User {
  @minLength(3)
  name: string = ''
}

const user = new User()
user.name = 'Ив'   // Ошибка!
user.name = 'Иван' // OK
Декораторы свойств не получают дескриптор, потому что свойства экземпляра создаются в конструкторе, а не на прототипе. Для перехвата нужно использовать Object.defineProperty на прототипе.

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

Фабрика — это функция, возвращающая декоратор. Позволяет параметризировать поведение:

// Фабрика с параметрами
function retry(times: number) {
  return function (
    target: any,
    propertyKey: string,
    descriptor: PropertyDescriptor
  ) {
    const originalMethod = descriptor.value

    descriptor.value = async function (...args: any[]) {
      let lastError: Error

      for (let attempt = 1; attempt <= times; attempt++) {
        try {
          return await originalMethod.apply(this, args)
        } catch (error) {
          lastError = error as Error
          console.log(`Попытка ${attempt} не удалась: ${error}`)
        }
      }

      throw lastError!
    }

    return descriptor
  }
}

class ApiService {
  @retry(3)
  async fetchData(url: string): Promise<any> {
    const response = await fetch(url)
    if (!response.ok) throw new Error(`HTTP ${response.status}`)
    return response.json()
  }
}

При composing декораторов важно понимать порядок:

function A(target: any, key: string, desc: PropertyDescriptor) {
  const orig = desc.value
  desc.value = function (...args: any[]) {
    console.log('A before')
    const result = orig.apply(this, args)
    console.log('A after')
    return result
  }
  return desc
}

function B(target: any, key: string, desc: PropertyDescriptor) {
  const orig = desc.value
  desc.value = function (...args: any[]) {
    console.log('B before')
    const result = orig.apply(this, args)
    console.log('B after')
    return result
  }
  return desc
}

class Service {
  @A  // применяется вторым (внешний)
  @B  // применяется первым (внутренний)
  process() { console.log('work') }
}

// При вызове service.process():
// A before → B before → work → B after → A after