Spec-Zone.ru › TypeScript 5.1

Декораторы

Введение

Дополнительные материалы:
Полное руководство по декораторам TypeScript

С введением классов в TypeScript и ES6 появились ситуации, требующие дополнительных возможностей для аннотирования или модификации классов и членов классов. Декораторы предоставляют способ добавления аннотаций и метапрограммного синтаксиса для объявлений классов и членов. Декораторы — это предложение стадии 2 для JavaScript и доступны как экспериментальная функция TypeScript.

ПРИМЕЧАНИЕ Декораторы — это экспериментальная функция, которая может измениться в будущих выпусках.

Чтобы включить экспериментальную поддержку декораторов, необходимо включить опцию компилятора experimentalDecorators либо в командной строке, либо в вашем tsconfig.json:

Командная строка:

tsc --target ES5 --experimentalDecorators

tsconfig.json:

{
  "compilerOptions": {
    "target": "ES5",
    "experimentalDecorators": true
  }
}

Декораторы

Декоратор — это специальный вид объявления, который может быть прикреплён к объявлению класса, методу, акцессору, свойству или параметру. Декораторы используют форму @expression, где expression должно вычисляться в функцию, которая будет вызвана во время выполнения с информацией об украшенном объявлении.

Например, при декораторе @sealed мы можем написать функцию sealed следующим образом:

function sealed(target) {
  // do something with 'target' ...
}

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

Если мы хотим настроить применение декоратора к объявлению, мы можем написать фабрику декоратора. Фабрика декоратора — это просто функция, которая возвращает выражение, которое будет вызываться декоратором во время выполнения.

Мы можем написать фабрику декоратора следующим образом:

function color(value: string) {
  // this is the decorator factory, it sets up
  // the returned decorator function
  return function (target) {
    // this is the decorator
    // do something with 'target' and 'value'...
  };
}

Композиция декораторов

Несколько декораторов могут быть применены к объявлению, например, в одной строке:

@f @g x

В несколько строк:

@f
@g
x

Когда несколько декораторов применяются к одному объявлению, их вычисление аналогично композиции функций в математике. В этой модели, когда композируют функции f и g, результирующая композиция (f ∘ g)(x) эквивалентна f(g(x)).

Таким образом, при вычислении нескольких декораторов для одного объявления в TypeScript выполняются следующие шаги:

  1. Выражения для каждого декоратора вычисляются сверху вниз.
  2. Затем результаты вызываются как функции снизу вверх.

Если мы используем фабрики декораторов, мы можем наблюдать этот порядок вычислений на следующем примере:

function first() {
  console.log("first(): factory evaluated");
  return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) {
    console.log("first(): called");
  };
}
 
function second() {
  console.log("second(): factory evaluated");
  return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) {
    console.log("second(): called");
  };
}
 
class ExampleClass {
  @first()
  @second()
  method() {}
}

Что будет выводить этот вывод в консоль:

first(): factory evaluated
second(): factory evaluated
second(): called
first(): called

Вычисление декораторов

Существует определённый порядок применения декораторов к различным объявлениям внутри класса:

  1. Декораторы параметров, за которыми следуют методы, аксессоры или декораторы свойств, применяются для каждого экземпляра члена.
  2. Декораторы параметров, за которыми следуют методы, аксессоры или декораторы свойств, применяются для каждого статического члена.
  3. Декораторы параметров применяются для конструктора.
  4. Декораторы класса применяются для класса.

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

Декоратор класса объявляется непосредственно перед объявлением класса. Декоратор класса применяется к конструктору класса и может использоваться для наблюдения, модификации или замены определения класса. Декоратор класса не может быть использован в файле объявления или в любом другом контексте окружения (например, на declare классе).

Выражение для декоратора класса будет вызываться как функция во время выполнения с конструктором украшенного класса в качестве единственного аргумента.

Если декоратор класса возвращает значение, он заменит объявление класса предоставленной функцией конструктора.

ПРИМЕЧАНИЕ Если вы выберете возврат новой функции конструктора, вы должны позаботиться о сохранении исходного прототипа. Логика, применяющая декораторы во время выполнения, не сделает этого за вас.

Следующий пример декоратора класса (@sealed) применяется к BugReport классу:

@sealed
class BugReport {
  type = "report";
  title: string;
 
  constructor(t: string) {
    this.title = t;
  }
}

Мы можем определить декоратор @sealed с помощью следующего объявления функции:

function sealed(constructor: Function) {
  Object.seal(constructor);
  Object.seal(constructor.prototype);
}

Когда @sealed выполняется, он запечатывает как конструктор, так и его прототип, и, следовательно, предотвращает любое дальнейшее добавление или удаление функциональности для этого класса во время выполнения путём доступа к BugReport.prototype или путём определения свойств на BugReport (обратите внимание, что классы ES2015 на самом деле являются всего лишь синтаксическим сахаром для функций конструкторов на основе прототипов). Этот декоратор не препятствует наследованию подклассов BugReport.

Далее пример того, как переопределить конструктор для установки новых значений по умолчанию.

function reportableClassDecorator<T extends { new (...args: any[]): {} }>(constructor: T) {
  return class extends constructor {
    reportingURL = "http://www...";
  };
}
 
@reportableClassDecorator
class BugReport {
  type = "report";
  title: string;
 
  constructor(t: string) {
    this.title = t;
  }
}
 
const bug = new BugReport("Needs dark mode");
console.log(bug.title); // Prints "Needs dark mode"
console.log(bug.type); // Prints "report"
 
// Note that the decorator _does not_ change the TypeScript type
// and so the new property `reportingURL` is not known
// to the type system:
bug.reportingURL;

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

Декоратор метода объявляется непосредственно перед объявлением метода. Декоратор применяется к описателю свойства метода и может использоваться для наблюдения, модификации или замены определения метода. Декоратор метода не может быть использован в файле объявления, перегрузке или в любом другом контексте окружения (например, в declare классе).

Выражение для декоратора метода будет вызываться как функция во время выполнения со следующими тремя аргументами:

  1. Функция конструктора класса для статического члена или прототип класса для экземпляра члена.
  2. Имя члена.
  3. Описатель свойства члена.

ПРИМЕЧАНИЕ Описатель свойства будет undefined если целевой скрипт меньше ES5.

Если декоратор метода возвращает значение, оно будет использоваться как описатель свойства метода.

ПРИМЕЧАНИЕ Возвращаемое значение игнорируется, если целевой скрипт меньше ES5.

Следующий пример декоратора метода (@enumerable) применяется к методу в Greeter классе:

class Greeter {
  greeting: string;
  constructor(message: string) {
    this.greeting = message;
  }
 
  @enumerable(false)
  greet() {
    return "Hello, " + this.greeting;
  }
}

Мы можем определить декоратор @enumerable с помощью следующего объявления функции:

function enumerable(value: boolean) {
  return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) {
    descriptor.enumerable = value;
  };
}

Декоратор @enumerable(false) здесь — фабрика декоратора. Когда декоратор @enumerable(false) вызывается, он изменяет свойство enumerable описателя свойства.

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

Декоратор аксессора объявляется непосредственно перед объявлением аксессора. Декоратор аксессора применяется к описателю свойства для аксессора и может использоваться для наблюдения, модификации или замены определений аксессора. Декоратор аксессора не может быть использован в файле объявления или в любом другом контексте окружения (например, в declare классе).

ПРИМЕЧАНИЕ TypeScript не позволяет украшать как get, так и set аксессор для одного члена. Вместо этого все декораторы для члена должны быть применены к первому аксессору, указанному в порядке документа. Это происходит потому, что декораторы применяются к описателю свойства, который объединяет как get, так и set аксессор, а не к каждому объявлению по отдельности.

Выражение для декоратора аксессора будет вызываться как функция во время выполнения со следующими тремя аргументами:

  1. Функция конструктора класса для статического члена или прототип класса для экземпляра члена.
  2. Имя члена.
  3. Описатель свойства члена.

ПРИМЕЧАНИЕ Описатель свойства будет undefined если целевой скрипт меньше ES5.

Если декоратор аксессора возвращает значение, оно будет использоваться как описатель свойства для члена.

ПРИМЕЧАНИЕ Возвращаемое значение игнорируется, если целевой скрипт меньше ES5.

Следующий пример декоратора аксессора (@configurable) применяется к члену Point класса:

class Point {
  private _x: number;
  private _y: number;
  constructor(x: number, y: number) {
    this._x = x;
    this._y = y;
  }
 
  @configurable(false)
  get x() {
    return this._x;
  }
 
  @configurable(false)
  get y() {
    return this._y;
  }
}

Мы можем определить декоратор @configurable с помощью следующего объявления функции:

function configurable(value: boolean) {
  return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) {
    descriptor.configurable = value;
  };
}

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

Декоратор свойства объявляется непосредственно перед объявлением свойства. Декоратор свойства не может быть использован в файле объявления или в любом другом контексте окружения (например, в declare классе).

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

  1. Функция конструктора класса для статического члена или прототип класса для экземпляра члена.
  2. Имя члена.

ПРИМЕЧАНИЕ Описатель свойства не предоставляется в качестве аргумента декоратору свойства из-за того, как декораторы свойств инициализируются в TypeScript. Это связано с тем, что в настоящее время нет механизма для описания свойства экземпляра при определении членов прототипа, а также нет способа наблюдать или изменять инициализатор свойства. Возвращаемое значение также игнорируется. Таким образом, декоратор свойства может использоваться только для наблюдения за тем, что свойство с определённым именем было объявлено для класса.

Мы можем использовать эту информацию для записи метаданных о свойстве, как показано в следующем примере:

class Greeter {
  @format("Hello, %s")
  greeting: string;

  constructor(message: string) {
    this.greeting = message;
  }

  greet() {
    let formatString = getFormat(this, "greeting");
    return formatString.replace("%s", this.greeting);
  }
}

Затем мы можем определить декоратор @format и функции getFormat с помощью следующих объявлений функций:

import "reflect-metadata";

const formatMetadataKey = Symbol("format");

function format(formatString: string) {
  return Reflect.metadata(formatMetadataKey, formatString);
}

function getFormat(target: any, propertyKey: string) {
  return Reflect.getMetadata(formatMetadataKey, target, propertyKey);
}

Декоратор @format("Hello, %s") здесь — фабрика декоратора. Когда @format("Hello, %s") вызывается, он добавляет запись метаданных для свойства с использованием функции Reflect.metadata из библиотеки reflect-metadata. Когда getFormat вызывается, он считывает значение метаданных для формата.

ПРИМЕЧАНИЕ Для этого примера требуется библиотека reflect-metadata. См. Метаданные для получения дополнительной информации о библиотеке reflect-metadata.

Декораторы параметров

Декоратор параметра объявляется непосредственно перед объявлением параметра. Декоратор параметра применяется к функции для объявления конструктора класса или метода. Декоратор параметра не может использоваться в файле объявления, перегрузке или в любом другом контексте среды (например, в классе declare).

Выражение для декоратора параметра будет вызываться как функция во время выполнения с тремя следующими аргументами:

  1. Функция-конструктор класса для статического члена или прототип класса для экземпляра члена.
  2. Имя члена.
  3. Порядковый индекс параметра в списке параметров функции.

ПРИМЕЧАНИЕ Декоратор параметра может использоваться только для наблюдения за тем, что параметр был объявлен в методе.

Значение возвращаемого результата декоратора параметра игнорируется.

Следующий пример декоратора параметра (@required) применяется к параметру члена класса BugReport:

class BugReport {
  type = "report";
  title: string;
 
  constructor(t: string) {
    this.title = t;
  }
 
  @validate
  print(@required verbose: boolean) {
    if (verbose) {
      return `type: ${this.type}\ntitle: ${this.title}`;
    } else {
     return this.title; 
    }
  }
}

Затем мы можем определить декораторы @required и @validate с помощью следующих объявлений функций:

import "reflect-metadata";
const requiredMetadataKey = Symbol("required");
 
function required(target: Object, propertyKey: string | symbol, parameterIndex: number) {
  let existingRequiredParameters: number[] = Reflect.getOwnMetadata(requiredMetadataKey, target, propertyKey) || [];
  existingRequiredParameters.push(parameterIndex);
  Reflect.defineMetadata( requiredMetadataKey, existingRequiredParameters, target, propertyKey);
}
 
function validate(target: any, propertyName: string, descriptor: TypedPropertyDescriptor<Function>) {
  let method = descriptor.value!;
 
  descriptor.value = function () {
    let requiredParameters: number[] = Reflect.getOwnMetadata(requiredMetadataKey, target, propertyName);
    if (requiredParameters) {
      for (let parameterIndex of requiredParameters) {
        if (parameterIndex >= arguments.length || arguments[parameterIndex] === undefined) {
          throw new Error("Missing required argument.");
        }
      }
    }
    return method.apply(this, arguments);
  };
}

Декоратор @required добавляет запись метаданных, которая помечает параметр как обязательный. Декоратор @validate затем оборачивает существующий метод print в функцию, которая проверяет аргументы перед вызовом исходного метода.

ПРИМЕЧАНИЕ Для этого примера требуется библиотека reflect-metadata. См. Метаданные для получения дополнительной информации о библиотеке reflect-metadata.

Метаданные

Некоторые примеры используют библиотеку reflect-metadata, которая добавляет полифилл для экспериментального API метаданных experimental metadata API. Эта библиотека пока не входит в стандарт ECMAScript (JavaScript). Однако после официального принятия декораторов как части стандарта ECMAScript эти расширения будут предложены для принятия.

Вы можете установить эту библиотеку через npm:

npm i reflect-metadata --save

TypeScript включает экспериментальную поддержку вывода определенных типов метаданных для объявлений, имеющих декораторы. Чтобы включить эту экспериментальную поддержку, необходимо установить опцию компилятора emitDecoratorMetadata либо в командной строке, либо в вашем файле tsconfig.json:

Командная строка:

tsc --target ES5 --experimentalDecorators --emitDecoratorMetadata

tsconfig.json:

{
  "compilerOptions": {
    "target": "ES5",
    "experimentalDecorators": true,
    "emitDecoratorMetadata": true
  }
}

При включении, если библиотека reflect-metadata была импортирована, дополнительная информация о типе во время проектирования будет доступна во время выполнения.

Мы можем увидеть это в действии в следующем примере:

import "reflect-metadata";
 
class Point {
  constructor(public x: number, public y: number) {}
}
 
class Line {
  private _start: Point;
  private _end: Point;
 
  @validate
  set start(value: Point) {
    this._start = value;
  }
 
  get start() {
    return this._start;
  }
 
  @validate
  set end(value: Point) {
    this._end = value;
  }
 
  get end() {
    return this._end;
  }
}
 
function validate<T>(target: any, propertyKey: string, descriptor: TypedPropertyDescriptor<T>) {
  let set = descriptor.set!;
  
  descriptor.set = function (value: T) {
    let type = Reflect.getMetadata("design:type", target, propertyKey);
 
    if (!(value instanceof type)) {
      throw new TypeError(`Invalid type, got ${typeof value} not ${type.name}.`);
    }
 
    set.call(this, value);
  };
}
 
const line = new Line()
line.start = new Point(0, 0)
 
// @ts-ignore
// line.end = {}
 
// Fails at runtime with:
// > Invalid type, got object not Point
 

Компилятор TypeScript введёт информацию о типе во время проектирования с помощью декоратора @Reflect.metadata. Вы можете рассматривать его как эквивалент следующего TypeScript:

class Line {
  private _start: Point;
  private _end: Point;

  @validate
  @Reflect.metadata("design:type", Point)
  set start(value: Point) {
    this._start = value;
  }
  get start() {
    return this._start;
  }

  @validate
  @Reflect.metadata("design:type", Point)
  set end(value: Point) {
    this._end = value;
  }
  get end() {
    return this._end;
  }
}

ПРИМЕЧАНИЕ Метаданные декораторов являются экспериментальной функцией и могут претерпеть изменения в будущих выпусках.

© 2012-2023 Microsoft
Licensed under the Apache License, Version 2.0.
https://www.typescriptlang.org/docs/handbook/decorators.html

Spec-Zone.ru

Настройки Оффлайн Что нового Помощь О нас
Spec-Zone .ru
спецификации, руководства, описания, API