Перейти к содержимому
13 / 37

Как типизировать decorator в TypeScript 5?


Полный ответ

Декоратор метода в TC39 Stage 3 получает два аргумента: оригинальную функцию и контекст:

TypeScript
// Декоратор метода — логирование вызовов

function log<This, Args extends unknown[], Return>(
  originalMethod: (this: This, ...args: Args) => Return,
  context: ClassMethodDecoratorContext<This, (this: This, ...args: Args) => Return>
) {
  const methodName = String(context.name);

  function replacement(this: This, ...args: Args): Return {
    console.log(`Calling ${methodName} with`, args);
    const result = originalMethod.call(this, ...args);
    console.log(`${methodName} returned`, result);
    return result;
  }

  return replacement;
}

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

const calc = new Calculator();
calc.add(2, 3);
// "Calling add with [2, 3]"
// "add returned 5"

Декоратор поля (field decorator) не получает значение — он возвращает инициализатор:

TypeScript
// Декоратор поля — валидация при присвоении

function min(minValue: number) {
  return function <This>(
    _value: undefined,
    context: ClassFieldDecoratorContext<This, number>
  ) {
    return function (this: This, initialValue: number): number {
      if (initialValue < minValue) {
        throw new Error(`${String(context.name)} must be >= ${minValue}, got ${initialValue}`);
      }
      return initialValue;
    };
  };
}

class Product {
  @min(0)
  price: number;

  constructor(price: number) {
    this.price = price;
  }
}

new Product(10);  // OK
// new Product(-5); // Error: price must be >= 0, got -5

Декоратор accessor — работает с парой get/set:

TypeScript
// auto-accessor + декоратор

function clamp(low: number, high: number) {
  return function <This>(
    target: ClassAccessorDecoratorTarget<This, number>,
    context: ClassAccessorDecoratorContext<This, number>
  ): ClassAccessorDecoratorResult<This, number> {
    return {
      get(this: This): number {
        return target.get.call(this);
      },
      set(this: This, value: number) {
        const clamped = Math.max(low, Math.min(high, value));
        target.set.call(this, clamped);
      },
      init(value: number): number {
        return Math.max(low, Math.min(high, value));
      },
    };
  };
}

class Slider {
  @clamp(0, 100)
  accessor value = 50;
}

const slider = new Slider();
slider.value = 200;
console.log(slider.value); // 100

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

TypeScript
// Декоратор класса — добавить toString

function serializable<T extends new (...args: unknown[]) => object>(
  Base: T,
  _context: ClassDecoratorContext<T>
) {
  return class extends Base {
    toString() {
      return JSON.stringify(this);
    }
  };
}

@serializable

class Config {
  constructor(
    public host: string,
    public port: number
  ) {}
}

console.log(new Config("localhost", 3000).toString());
// '{"host":"localhost","port":3000}'

Реальные кейсы

Замер производительности метода:

TypeScript
function measure<This, Args extends unknown[], Return>(
  originalMethod: (this: This, ...args: Args) => Return,
  context: ClassMethodDecoratorContext
) {
  const name = String(context.name);
  return function (this: This, ...args: Args): Return {
    const start = performance.now();
    const result = originalMethod.call(this, ...args);
    console.log(`${name}: ${(performance.now() - start).toFixed(2)}ms`);
    return result;
  };
}

Ошибка: путать Stage 3 и legacy декораторы:

TypeScript
// ❌ Legacy (experimentalDecorators) — другая сигнатура

function oldDecorator(target: object, key: string, descriptor: PropertyDescriptor) {}

// ✅ Stage 3 (TypeScript 5+) — context-объект

function newDecorator(method: Function, context: ClassMethodDecoratorContext) {}

Резюме

Декораторы Stage 3 в TypeScript 5 типизируются через контекстные интерфейсы: ClassMethodDecoratorContext, ClassFieldDecoratorContext, ClassAccessorDecoratorContext, ClassDecoratorContext. Каждый получает оригинальное значение и context с метаданными. Не нужно включать experimentalDecorators — это новый стандарт.

Как типизировать decorator в TypeScript 5? | JScriptiser