В этом блоге мы углубимся в то, что такое декоратор и как он работает в Typescript (представленный в версии 5.0), а также коротко выделим разницу с декораторами JavaScript. Мы также кратко обсудим устаревшие декораторы для Typescript.

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

Декораторы связаны с шаблоном декоратора, шаблоном проектирования. Декоратор расширяет (украшает) функциональность конкретного объекта/метода, обертывая его. Это гарантирует, что исходная функциональность не будет изменена, но при этом будет возможность расширить ее новыми функциями.

Декорирование функции может устранить перекрестные проблемы, такие как ведение журнала, отслеживание или мониторинг из бизнес-логики. Следовательно, декораторы полезны при попытке следовать Шаблону единой ответственности.

Как работают декораторы в Typescript?

Важно подчеркнуть, что Typescript реализовал текущую стадию 3 Предложение для декораторов в JavaScript. Это изменение появилось в Typescript 5.0 и не стоит за флагом experimentalDecorators, как предыдущая реализация декоратора. Предыдущая реализация основана на Stage 2 и теперь может считаться устаревшей. С учетом этого уточнения декоратор в Typescript имеет следующее определение:

function Decorator(target: unknown, context: unknown) {
  // The kind of element, this can be one of ['class', 'method', 'getter', 'setter', 'field', 'accessor']
  context.kind;

  // The element's name is either a Symbol or a String
  context.name;

  // Indicator if the target is private
  context.private;

  // Indicator if the target is static
  context.static;

  // An object containing either get, set or both properties. It allows for reading/writing the underlying value of an object
  context.access;

  // A function that can be called to register a callback that is evaluated either when the class is defined or when an instance is created
  context.addInitializer(() => {});
}

Как следует из свойства kind параметра context, декорирование параметров в функциях/методах не поддерживается. Это связано с тем, что предложение декоратора параметров должно быть на этапе 3. Тем не менее, мы можем ожидать поддержку параметров в будущем.

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

Давайте начнем с декораторов классов и посмотрим, как они реализуются и применяются. Имейте в виду, что внешняя функция — это просто фабрика, позволяющая настроить декоратор, поэтому фактический декоратор называется ClassDecorator.

// Decorator Factory
function ColouredHouse(colour: string = "Blue") {
  return function ClassDecorator<C extends new (...args: any[]) => any>(
    target: C,
    ctx: ClassDecoratorContext,
  ) {
    const className = ctx.name?.toString();
    console.info("ClassName is", className);

    return class extends target {
      colour = colour;
    };
  };
}

Сигнатура нашего декоратора, ClassDecorator, использует дженерики. Наш универсальный C имеет ограничение new (…args: any[]) =› any, которое гарантирует, что C — это класс. Этот декоратор заключен в фабричный метод ColouredHouse, который возвращает ClassDecorator с цветом инициализируется предоставленной строкойили значением по умолчанию Blue. В приведенном выше фрагменте ClassDecorator возвращает новый класс, который расширяет цель и добавляет «новое» значение color. цепочку прототипов можно использовать как альтернативу возврату нового класса. Это показано в следующем фрагменте.

return function ClassDecorator<C extends new (...args: any[]) => any>(
  target: C,
  ctx: ClassDecoratorContext,
) {
  const className = ctx.name?.toString();
  console.info("ClassName is", className);

  target.prototype.colour = colour;
};

Теперь, когда у нас есть декоратор классов, давайте применим его к следующим демонстрационным классам SmallHouse и Largehouse:

@ColouredHouse()
class Largehouse {
  constructor(
    private adress: string,
    private floors = 3,
    private colour?: string,
  ) {}

  toString(): string {
    return `This is a Largehouse with ${this.floors} Floors @ ${this.adress} being ${this.colour}`;
  }
}

@ColouredHouse("Green")
class SmallHouse {
  constructor(
    private adress: string,
    private floors = 1,
    private colour?: string,
  ) {}

  toString(): string {
    return `This is a SmallHouse with ${this.floors} Floors @ ${this.adress} being ${this.colour}`;
  }
}

const firstHouse = new SmallHouse("5331 Rexford Court, Montgomery AL 36116");
const secondHouse = new Largehouse("8642 Yule Street, Arvada CO 80007");

console.info(firstHouse.toString());
console.info(secondHouse.toString());

Выполнение приведенного выше кода приведет к следующему выводу консоли. Короткое замечание: декоратор всегда оценивается перед созданием экземпляра класса:

ClassName is Largehouse
ClassName is SmallHouse
This is a SmallHouse with 1 Floors @ 5331 Rexford Court, Montgomery AL 36116 being Green
This is a Largehouse with 3 Floors @ 8642 Yule Street, Arvada CO 80007 being Blue

Как видно из вывода, цвет инициализируется значениями, определенными в декораторах. Кроме того, он даже переопределит значение color, если оно указано в конструкторе. Это означает, что new SmallHouse("5331 Rexford Court, Montgomery AL 36116", 1, 'White'); по-прежнему будет console.log Зеленый.

Полевой декоратор

Декоратор поля отличается от других декораторов тем, что при декорировании он не имеет «цели» (ввода). Вместо этого он позволяет пользователю предоставить функцию инициализации, которая получает начальное значение (в приведенном ниже фрагменте оно не используется, поэтому _), а затем возвращает любое значение того же типа. Обратите внимание, что в следующем фрагменте снова используется фабрика декораторов, фактическим декоратором поля является FieldDecorator.

// Decorator Factory
function Deprecation() {
  return function FieldDecorator<C, V>(
    target: undefined,
    ctx: ClassFieldDecoratorContext<C, V>,
  ) {
    const fieldName = ctx.name.toString();
    console.info("FieldName is", fieldName);

    return function (_: V) {
      console.warn(
        `${fieldName} is officially deprecated and will be removed in a future version`,
      );
      return false;
    };
  };
}

Сигнатура нашего декоратора, FieldDecorator, использует дженерики. Наш общий C обозначает класс, а V — тип значения в этом примере логическое значение. Он печатает предупреждение об устаревании через console.warn, а затем инициализирует поле значением false. Следовательно, в следующем фрагменте значение example.legacyFlag будет false вместо верно.

class Product {
  @Deprecation()
  public legacyFlag: boolean = true;
}

const example = new Product();
console.info(`Value of legacyFlag=${example.legacyFlag}`);

Выполнение приведенного выше фрагмента даст следующий результат:

FieldName is legacyFlag
legacyFlag is officially deprecated and will be removed in a future version
Value of legacyFlag=false

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

В следующем фрагменте кода демонстрируется реализация декоратора метода с именем MethodDecorator, который добавляет стандартное ведение журнала к оформленной функции.

// Decorator Factory
function LeveledLog(level: string) {
  return function MethodDecorator(
    target: Function,
    ctx: ClassMethodDecoratorContext,
  ) {
    const methodName = ctx.name.toString();

    return function decoratedTarget(this: unknown, ...args: unknown[]) {
      console.info(
        `[${level}] ${methodName} was called with ${JSON.stringify(args)}`,
      );
      return target.call(this, args);
    };
  };
}

Как видно из приведенного выше фрагмента, цель будет обернута специальной функцией, которая добавит нашу функциональность (ведение журнала) и позаботится о вызове исходной цели. Это также можно использовать для других целей, таких как измерение времени выполнения. Крайне важно обеспечить надлежащую обработку ошибок, чтобы гарантировать, что исходная функциональность останется нетронутой. Затем декоратор можно применить следующим образом:

class HelloWorld {
  constructor() {}

  @LeveledLog("Info")
  public greet(message: string): string {
    return `Hello ${message}`;
  }
}

const target = new HelloWorld();
console.info(`Generated by Hello World ${target.greet("World")}`);

Приведенный выше код выдаст следующий вывод:

[Info] greet was called with ["World"]
Generated by Hello World Hello World

В чем разница между декораторами Typescript и JavaScript?

Поскольку Typescript следует за JavaScript Proposal, в долгосрочной перспективе не будет много различий. Однако, поскольку этап 3 значительно отличается от этапа 2, транспиляторам JavaScript, таким как Babel, по-прежнему необходимо (полностью) реализовать новое предложение. Таким образом, в настоящее время JavaScript не поддерживает весь диапазон, упомянутый выше, а вместо этого реализует только Декораторы классов и методов. Кроме того, Typescript поставляется с правильными типами для декораторов, которые обеспечивают правильное использование и позволяют избежать неожиданностей во время выполнения.

К сожалению, мы попали в классическую ловушку: «Старое устарело, а новое еще не готово!» На данный момент лучше продолжать использовать старую вещь.

Взято из официального FAQ