TypeScript. Декораторы
С введением классов в TypeScript и ES6 в настоящее время существуют определенные сценарии, которые требуют дополнительных возможностей для поддержки аннотирования или модификации классов и членов класса. Декораторы позволяют, как добавить аннотации, так и использовать синтаксис метапрограммирования при объявлении классов и их членов. Декораторы предлагают удобный декларативный синтаксис для изменения формы объявления класса.
Декораторы еще не вошли в стандарт EcmaScript и на момент написания статьи находятся на стадии stage 1 proposal. Однако, несмотря на то, что они являются экспериментальной функцией и в дальнейшем возможны изменения, декораторы активно используются в Angular2. Ангуларовские @NgModule
, @Component
, @Injectable
- это не что иное, как эти самые декораторы. Поэтому данную тему я решил не откладывать до момента официального вхождения в стандарт. Попробую разобраться хотя бы в том, как они работают, чтобы в дальнейшем было проще с изучением Angular2.
Для того чтобы включить поддержку декораторов в TS, нужно добавить опцию experimentalDecorators
в командной строке:
tsc --target ES5 --experimentalDecorators
либо в файле tsconfig.json
:
{
"compilerOptions": {
"target": "ES5",
"experimentalDecorators": true
}
}
Декоратор
Декоратор представляет собой объявление вида @expression
, где expression
- функция, которая будет вызвана во время выполнения с информацией о декорируемом элементе. Декорировать можно классы, методы, аксессоры, свойства или параметры.
Например, для декоратора @sealed
(запечатанный) можно было бы написать функцию sealed
:
function sealed(target) {
// запечатываем target
}
Фабрика декораторов
Если мы хотим настроить, как декоратор применяется к элементу, мы можем написать фабрику декораторов. Это просто функция, которая может принимать некоторые параметры и возвращать декоратор.
Пример фабрики декораторов:
function color(value: string) { // это фабрика
return function (target) { // это декоратор
// делаем что-то с 'target' и 'value'...
}
}
Композиция декораторов
Несколько декораторов могут быть применены к объявлению следующим образом:
-
в одну строку:
@f @g x
-
в несколько строк:
@f @g x
Несколько декораторов вместе будут работать подобно функциям в математике, т.е. так: f(g(x)).
Декораторы классов
Декоратор класса объявляется непосредственно перед объявлением самого декорируемого класса. Применяется к конструктору класса и может быть использован для наблюдения, модификации или замены определения класса.
Выражение для декоратора класса будет вызываться во время выполнения как функция с конструктором декорируемого класса в качестве единственного аргумента.
Пример декоратора класса (@sealed
) для класса Greeter
:
@sealed
class Greeter {
greeting: string;
constructor(message: string) {
this.greeting = message;
}
greet() {
return "Hello, " + this.greeting;
}
}
Для декоратора sealed
можем записать следующую функцию:
function sealed(constructor: Function) {
Object.seal(constructor);
Object.seal(constructor.prototype);
}
Когда декоратор @sealed
будет выполнен, он запечатает и конструктор, и прототип класса Greeter
.
Декораторы методов
Декоратор метода объявляется непосредственно перед самим методом. Декоратор применяется на дескрипторе свойства для данного метода и может быть использован для наблюдения, модификации или замены определения метода.
Выражение для декоратора метода будет вызвано как функция с тремя аргументами:
-
Либо функция-конструктор класса для статического метода, либо прототип класса.
-
Название метода.
-
Дескриптор свойства для данного метода.
Примечание: если версия целевого скрипта ниже ES5, то дескриптор свойства будет
undefined
.
Пример использования декоратора @enumerable
, который включает или отключает доступность метода при перечислении свойств объекта:
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;
};
}
Декораторы аксессоров
Декоратор аксессора объявляется непосредственно перед объявлением самого аксессора. Декоратор применяется на дескрипторе свойства для данного аксессора и может быть использован для наблюдения, модификации или замены его определения.
TypeScript не позволяет применять декоратор и к геттеру, и к сеттеру. Декоратор должен быть объявлен для первого аксессора, указанного в коде. Это происходит потому, что декоратор применяется для дескриптора свойства, которое содержит и get
, и set
.
Выражение для декоратора аксессора будет вызвано как функция с тремя аргументами:
-
Либо функция-конструктор класса для статического члена, либо прототип класса.
-
Название члена.
-
Дескриптор свойства для данного члена.
Примечание: если версия целевого скрипта ниже ES5, то дескриптор свойства будет
undefined
.
Если декоратор аксессора возвращает значение, то оно будет использовано как дескриптор свойства для данного члена.
В следующем примере декоратор аксессоров @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;
};
}
Декораторы свойств
Все то же самое. Объявляется непосредственно перед объявлением свойства. Выражение для декоратора свойства будет вызвано как функция с двумя аргументами:
-
Либо функция-конструктор класса для статического члена, либо прототип класса.
-
Название свойства.
Если декоратор свойства возвращает значение, то оно будет использовано как дескриптор.
Примечание: если версия целевого скрипта ниже ES5, то возвращаемое значение будет проигнорировано.
Можно использовать декоратор для сохранения метаданных о свойстве класса:
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")
- фабрика декораторов. Когда декоратор вызывается, он добавляет запись метаданных для свойства с использованием функции Reflect.metadata
из библиотеки reflect-metadata
(ниже про нее будет написано). Когда вызывается функция getFormat
, она считывает метаданные и возвращает требуемый формат.
Декораторы параметров
Объявляется непосредственно перед объявлением параметра. Применяется для функции-конструктора класса или для метода.
Выражение для декоратора параметра будет вызвано как функция с тремя аргументами:
-
Либо функция-конструктор класса для статического члена, либо прототип класса.
-
Название члена.
-
Порядковый номер параметра в списке аргументов функции.
Декораторы применяется только для наблюдения за параметром, объявленным в методе.
Значение, возвращаемое декоратором параметра, будет проигнорировано.
В следующем примере декоратор параметра @required
применяется к параметру name
функции greet
:
class Greeter {
greeting: string;
constructor(message: string) {
this.greeting = message;
}
@validate
greet(@required name: string) {
return "Hello " + name + ", " + this.greeting;
}
}
Декораторы @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
оборачивает метод greet
в функцию, которая предварительно выполняет валидацию аргументов. В данном примере также требуется установка дополнительной библиотеки reflect-metadata
, о которой речь пойдет чуть ниже.
Метаданные
В некоторых примерах была использована библиотека reflect-metadata
, которая является полифиллом для эксперементального metadata API. Библиотека не является частью стандарта ECMAScript (JavaScript). Однако, как только декораторы войдут в стандарт, эти расширения будут предложены для принятия.
Установить библиотеку можно с помощью npm:
npm i reflect-metadata --save
Для того чтобы включить поддержку метаданных в TypeScript нужно добавить опцию emitDecoratorMetadata
в консоли:
tsc --target ES5 --experimentalDecorators --emitDecoratorMetadata
либо в файле tsconfig.json
:
{
"compilerOptions": {
"target": "ES5",
"experimentalDecorators": true,
"emitDecoratorMetadata": true
}
}
Когда библиотека reflect-metadata
импортирована, дополнительная design-time информация о типах становится доступной в рантайме.
Мы можем увидеть в действии это в следующем примере:
import "reflect-metadata";
class Point {
x: number;
y: number;
}
class Line {
private _p0: Point;
private _p1: Point;
@validate
set p0(value: Point) { this._p0 = value; }
get p0() { return this._p0; }
@validate
set p1(value: Point) { this._p1 = value; }
get p1() { return this._p1; }
}
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.");
}
}
}
Компилятор TypeScript внедрит информацию о типе с помощью декоратора @Reflect.metadata
. Вот эквивалентный код класса:
class Line {
private _p0: Point;
private _p1: Point;
@validate
@Reflect.metadata("design:type", Point)
set p0(value: Point) { this._p0 = value; }
get p0() { return this._p0; }
@validate
@Reflect.metadata("design:type", Point)
set p1(value: Point) { this._p1 = value; }
get p1() { return this._p1; }
}
Уф. Довольно сложная тема. Поэтому сильно глубоко проникнуться не получилось. Получил лишь легкое понимание принципа работы декораторов. Соответственно и статья вышла обычным сухим переводом официальной документации. Если у кого-то есть замечания, предложения или более доступные для понимания объяснения и примеры по данной теме, то буду рад комментариям. Спасибо за понимание.
P. S. Нашел на хабре добротную статью про декораторы в TypeScript с интересными примерами. Советую к прочтению. Да прибудет с вами Сила ;)