- PVSM.RU - https://www.pvsm.ru -

Темная сторона TypeScript — @декораторы на примерах

Декораторы — это невероятно круто. Они позволяют описывать мета информацию прямо в объявлении класса, группируя все в одном месте и избегая дублирования. Ужасно удобно. Однажды попробовав, вы уже никогда не согласитесь писать по-старому.

Однако, несмотря на всю полезность, декораторы в TypeScript (заявлены [1] также на стандарт) не так просты, как хотелось бы. Работа с ними требует навыков джедая, так как необходимо разбираться в объектной модели JavaScript [2] (ну, вы поняли, о чем я), API несколько запутанный и, к тому же, еще не стабильный. В этой статье я немного расскажу об устройстве декораторов и покажу несколько конкретных приемов, как поставить эту темную силу на благо front-end разработки.

Помимо TypeScript, декораторы доступны в Babel [3]. В этой статье рассматривается только реализация в TypeScript.

Темная сторона TypeScript — @декораторы на примерах - 1

Темная сторона TypeScript — @декораторы на примерах - 2  Основы

Декорировать в TypeScript можно классы, методы, параметры метода, методы доступа свойства (accessors) и поля.

Почему я использую термин 'поле', а не 'свойство' как в официальной документации

В TypeScript термин "поле" обычно не используется, и поля называют также свойствами [4] (property). Это создает большую путаницу, т.к. разница есть. Если мы объявляем свойство с методами доступа get/set, то в объявлении класса появляется [5] вызов Object.defineProperty и в декораторе доступен дескриптор, а если объявляем просто поле (в терминах C# и Java) — то не появляется [6] ничего, и, соответственно, дескриптор не передается в декоратор. Это определяет сигнатуру декораторов, поэтому я использую термин "поле", чтобы отличать их от свойств с методами доступа.

В общем случае, декоратор — это выражение, предваренное символом "@", которое возвращает функцию определенного вида (разного в каждом случае [7]). Собственно, можно просто объявить такую функцию и использовать ее имя в качестве выражения декоратора:

function MyDecorator(target, propertyKey, descriptor) {
    // ...
}
class MyClass {
    @MyDecorator
    myMethod() {
    }
}

Однако можно использовать любое другое выражение, которое вернет такую функцию. Например, можно объявить другую функцию, которая будет принимать параметрами дополнительную информацию, и возвращать соответствующую лямбду. Тогда в качестве декоратора будем использовать выражение "вызов функции MyAdvancedDecorator".

function MyAdvancedDecorator(info?: string) {
   return (target, propertyKey, descriptor) => {
        // ..
   };
}
class MyClass {
    @MyAdvancedDecorator("advanced info")
    myMethod() {
    }
}

Здесь самый обычный вызов функции, поэтому, даже если мы не передаем параметры, все равно нужно писать скобки "@MyAdvancedDecorator()". Собственно, это два основных способа объявления декораторов.

В процессе компиляции объявление декоратора приводит к появлению вызова нашей функции в определении класса. То есть там, где вызываются Object.defineProperty, заполняется прототип класса и все такое. Как именно это происходит — важно знать, т.к. это объясняет, когда вызывается декоратор, что представляют собой параметры нашей функции, почему они именно такие, а также что и как в декораторе можно сделать. Ниже приведен упрощенный код, в который компилируется наш класс с декоратором:

var __decorateMethod = function (decorators, target, key) {
    var descriptor = Object.getOwnPropertyDescriptor(target, key);
    for (var i = decorators.length - 1; i >= 0; i--) {
        var decorator = decorators[i];
        descriptor = decorator(target, key, descriptor) || descriptor; // Вызов функции декоратора
    }
    Object.defineProperty(target, key, descriptor);
};

// Объявление класса MyClass
var MyClass = (function () {
    function MyClass() {} // Конструктор
    MyClass.prototype.myMethod = function () { }; // метод myMethod

    // Вызов декораторов
    __decorateMethod([
        MyAdvancedDecorator("advanced info") // Вычисление выражения декоратора, и получение функции 
    ], MyClass.prototype, "myMethod");
    return MyClass;
}());

В таблице ниже приведено описание функции для каждого вида декораторов, а также ссылки на примеры в TypeScript Playground [8], где можно посмотреть, во что точно компилируются декораторы и попробовать их в действии.

Вид декоратора Сигнатура функции
Декоратор класса [9]
Пример в playground [10]

@MyDecorator 
class MyClass {}

function MyDecorator<TFunction extends Function>(target: TFunction): TFunction {
  return target;
}

  • target — конструктор класса
  • returns — конструктор класса или null. Если вернуть конструктор, то он заменит оригинальный. При этом необходимо также настроить прототип в новом конструкторе.

Декоратор метода [11]
Пример в playground [12]

class MyClass {
  @MyDecorator
  myMethod(){}
}

function MyDecorator(target: Object, propertyKey: string | symbol, descriptor: TypedPropertyDescriptor<any>): TypedPropertyDescriptor<any> {
  return descriptor;
}

  • target — прототип класса
  • propertyKey — имя метода (сохраняется при минификации); в текущей реализации тип — string
  • descriptorдескриптор [13] метода* [14]
  • returns — дескриптор метода* [14] или null

Декоратор статического метода [11]
Пример в playground [15]

class MyClass {
  @MyDecorator
  static myMethod(){}
}

function MyDecorator(target: Function, propertyKey: string | symbol, descriptor: TypedPropertyDescriptor<any>): TypedPropertyDescriptor<any> {
  return descriptor;
}

  • target — конструктор класса
  • propertyKey — имя метода (сохраняется при минификации); в текущей реализации тип — string
  • descriptorдескриптор [13] метода* [14]
  • returns — дескриптор метода* [14] или null

Декоратор методов доступа [16]
Пример в playground [17]

class MyClass {
  @MyDecorator
  get myProperty(){}
}

Аналогично методу. Декоратор следует применять к первому методу доступа (get или set), в порядке объявления в классе.
Декоратор параметра [18]
Пример в playground [19]

class MyClass {
  myMethod(
    @MyDecorator val){
    }
}

function MyDecorator(target: Object, propertyKey: string | symbol, index: number): void { }

  • target — прототип класса
  • propertyKey — имя метода (сохраняется при минификации); в текущей реализации тип — string
  • index — индекс параметра в списке параметров
  • returns — void


Декоратор поля (свойства) [20]
Пример в playground [21]

class MyClass {
  @MyDecorator
  myField: number;
}

function MyDecorator(target: Object, propertyKey: string | symbol): TypedPropertyDescriptor<any> {
return null;
}

  • target — прототип класса
  • propertyKey — имя поля (сохраняется при минификации); в текущей реализации тип — string
  • returns — null или дескриптор [13] свойства; если вернуть дескриптор, то он будет использован для вызова Object.defineProperty; однако, при подключении библиотеки reflect-metadata этого не происходит (это баг в reflect-metadata [22])

Декоратор статического поля (свойства) [20]
Пример в playground [23]

class MyClass {
  @MyDecorator
  static myField;
}

function MyDecorator(target: Function, propertyKey: string | symbol): TypedPropertyDescriptor<any> {
return null;
}

  • target — конструктор класса
  • propertyKey — имя поля (сохраняется при минификации); в текущей реализации тип — string
  • returns — null или дескриптор [13] свойства; если вернуть дескриптор, то он будет использован для вызова Object.defineProperty; однако при подключении библиотеки reflect-metadata этого не происходит (это баг в reflect-metadata [22])

Интерфейсы Декораторы интерфейсов и их членов не поддерживаются.
Объявления типов Декораторы в объявлениях типов (ambient declarations) не поддерживаются.
Функции и переменные вне класса Декораторы вне класса не поддерживаются.

Интерфейс TypedPropertyDescriptor<T>, фигурирующий в сигнатуре декораторов методов и свойств объявлен следующим образом:

interface TypedPropertyDescriptor<T> {
    enumerable?: boolean;
    configurable?: boolean;
    writable?: boolean;
    value?: T;
    get?: () => T;
    set?: (value: T) => void;
}

Если указать в объявлении декоратора конкретный тип T для TypedPropertyDescriptor, то можно ограничить тип свойств, к которым декоратор применим. Что означают члены этого интерфейса — можно посмотреть здесь [13]. Если коротко, для метода value содержит собственно сам метод, для поля — значение, для свойства — get и set содержат соответствующие методы доступа.

Темная сторона TypeScript — @декораторы на примерах - 3 Настройка среды

Поддержка декораторов экспериментальная и может измениться в будущих релизах (в TypeScript 2.0 [24] не изменилась). Поэтому необходимо добавить experimentalDecorators: true в tsconfig.json. Кроме того, декораторы доступны только если target: es5 или выше.

tsconfig.json

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

Темная сторона TypeScript — @декораторы на примерах - 4  Важно!!!о target: ES3 и JSFiddle

Важно не забыть указать опцию target — ES5 при работе с декораторами. Если этого не сделать, то код скомпилируется без ошибок, но работать будет по-другому (это баг в компиляторе TypeScript [25]). В частности, декораторам методов и свойств не будет передаваться третий параметр, а их возвращаемое значение будет игнорироваться.

Эти феномены можно наблюдать в JSFiddle (это уже баг в JSFiddle [26]), поэтому в данной статье я не размещаю примеры в JSFiddle.

Тем не менее, есть обходное решение для этих багов. Нужно просто самим получать дескриптор, и самим же его обновлять. Например, вот реализация декоратора @safe [27], которая работает как с target ES3, так и с ES5.

Для использования информации о типах необходимо также добавить emitDecoratorMetadata: true.

tsconfig.json

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

Для использования класса Reflect [28] необходимо установить дополнительный пакет reflect-metadata [29]:

npm install reflect-metadata --save

И в коде:

import "reflect-metadata";

Однако если вы используете Angular 2, то ваша система сборки уже может содержать в себе реализацию Reflect, и после установки пакета reflect-metadata вы можете получить runtime ошибку Unexpected value 'YourComponent' exported by the module 'YourModule'. В этом случае лучше установить только typings [30].

typings install dt~reflect-metadata --global --save

Итак, перейдем к практике. Рассмотрим несколько примеров, демонстрирующих возможности декораторов.

@safeавтоматическая обработка ошибок внутри функции

Темная сторона TypeScript — @декораторы на примерах - 5

Допустим, у нас часто встречаются второстепенные функции, ошибки внутри которых мы хотели бы игнорировать. Писать каждый раз try/catch громоздко, на помощь приходит декоратор:

Реализация декоратора

function safe(target: Object, propertyKey: string, descriptor: TypedPropertyDescriptor<any>): TypedPropertyDescriptor<any> {
  // Запоминаем исходную функцию
  var originalMethod = descriptor.value;
  // Подменяем ее на нашу обертку
  descriptor.value = function SafeWrapper () {
    try {
      // Вызываем исходный метод
      originalMethod.apply(this, arguments);
    } catch(ex) {
      // Просто выводим в консоль, исполнение кода будет продолжено
      console.error(ex);
    }
  };
  // Обновляем дескриптор
  return descriptor;
}

class MyClass {
    @safe public foo(str: string): boolean {
     return str.length > 0; // если str == null, будет ошибка
  }
}
var test = new MyClass();
console.info("Starting...");
test.foo(null); 
console.info("Continue execution");

Результат выполнения:

Темная сторона TypeScript — @декораторы на примерах - 6

Попробовать в действии в Plunker [31]
Посмотреть в Playground [32]

@OnChangeзадание обработчика изменения значения поля

Темная сторона TypeScript — @декораторы на примерах - 7

Допустим, при изменении значения поля нужно выполнить какую-то логику. Можно, конечно, определить свойство с get/set методами, и в set поместить нужный код. А можно сократить объем кода, объявив декоратор:

Реализация декоратора

function OnChange<ClassT, T>(callback: (ClassT, T) => void): any {
    return (target: Object, propertyKey: string | symbol) => {
      // Необходимо задействовать существующий дескриптор, если он есть.
      // Это позволит объявять несколько декораторов на одном свойстве.
      var descriptor = Object.getOwnPropertyDescriptor(target, propertyKey) 
        || {configurable: true, enumerable: true};
      // Подменяем или объявляем get и set
      var value: T;
      var originalGet = descriptor.get || (() => value);
      var originalSet = descriptor.set || (val => value = val);
      descriptor.get = originalGet;
      descriptor.set = function(newVal: T) {
        // Внимание, если определяем set через function, 
        // то this - текущий экземпляр класса,
        // если через лямбду, то this - Window!!!
        var currentVal = originalGet.call(this);
        if (newVal != currentVal) {
          // Вызываем статический метод callback с двумя параметрами
          callback.call(target.constructor, this, newVal);
        }
        originalSet.call(this, newVal);
      };
      // Объявляем новое свойство, либо обновляем дескриптор
      Object.defineProperty(target, propertyKey, descriptor);
      return descriptor;
    }
}

Обратите внимание, мы вызываем defineProperty и возвращаем дескриптор из декоратора. Это связано с багом в reflect-metadata [22], из-за которого для декоратора полей возвращаемое значение игнорируется.

class MyClass {
    @OnChange(MyClass.onFieldChange)
    public mMyField: number = 42;

    static onFieldChange(self: MyClass, newVal: number): void {
      console.info("Changing from " + self.mMyField + " to " + newVal);
    }
}
var test = new MyClass();
test.mMyField = 43;
test.mMyField = 44;

Результат выполнения:

Темная сторона TypeScript — @декораторы на примерах - 8

» Попробовать в действии в Plunker [33]
» Посмотреть в Playground [34]
Нам пришлось обработчик объявить как static, т.к. трудно сосласться на экземплярный метод. Вот альтернативный вариант со строковым параметром [35], и другой с использованием лямбды [36].

@Injectвнедрение зависимостей

Темная сторона TypeScript — @декораторы на примерах - 9

Одной из интересных особенностей декораторов является возможность получать информацию о типе декорируемого свойства или параметра (скажем "спасибо" Angular, т.к. сделано было специально для него). Чтобы это заработало, нужно подключить библиотеку reflect-metadata, и включить опцию emitDecoratorMetadata (см. выше). После этого для свойств, которые имеют хотя бы один декоратор, можно вызвать Reflect.getMetadata с ключем "design:type", и получить конструктор соответствующего типа. Ниже простая реализация декоратора @Inject, который использует этот прием для внедрения зависимостей:

Реализация декоратора

// Объявляем декоратор
function Inject(target: Object, propKey: string): any {
    // Получаем конструктор типа свойства 
    // (в примере ниже это будет конструктор класса ILogService)
    var propType = Reflect.getMetadata("design:type", target, propKey);
    // Переопределяем декорируемое свойство
    var descriptor = {
        get: function () {
          // this - текущий объект класса
          var serviceLocator = this.serviceLocator || globalSericeLocator;
          return serviceLocator.getService(propType);  

        }
    };
    Object.defineProperty(target, propKey, descriptor);
    return descriptor;
}

Обратите внимание, мы вызываем defineProperty и возвращаем дескриптор из декоратора. Это связано с багом в reflect-metadata [22], из-за которого для декоратора полей возвращаемое значение игнорируется.

// Использовать интерфейс, к сожалению, не получится
abstract class ILogService {
    abstract log(msg: string): void;
} 
class Console1LogService extends ILogService {
  log(msg: string) { console.info(msg);  }
}
class Console2LogService extends ILogService {
  log(msg: string) { console.warn(msg); }
}
var globalSericeLocator = new ServiceLocator();
globalSericeLocator.registerService(ILogService, new ConsoleLogService1());
class MyClass {
  @Inject
  private logService: ILogService;
  sayHello() {
    this.logService.log("Hello there");
  }
}
var my = new MyClass();
my.sayHello();
my.serviceLocator = new ServiceLocator();
my.serviceLocator.registerService(ILogService, new ConsoleLogService2());
my.sayHello();

Реализация класса ServiceLocator

class ServiceLocator {
  services: [{interfaceType: Function, instance: Object }] = [] as any;

  registerService(interfaceType: Function, instance: Object) {
    var record = this.services.find(x => x.interfaceType == interfaceType);
    if (!record) {
      record = { interfaceType: interfaceType, instance: instance};
      this.services.push(record);
    } else {
      record.instance = instance;
    }
  }
  getService(interfaceType: Function) {
    return this.services.find(x => x.interfaceType == interfaceType).instance;
  }
}

Как видно, мы просто объявляем поле logService, а декоратор уже самостоятельно определяет его тип, и задает метод доступа, который получает соответствующий экземпляр сервиса. Красиво и удобно. Результат выполнения:

Темная сторона TypeScript — @декораторы на примерах - 10

» Попробовать в Plunker [37]
» Посмотреть в Playground [38]

@JsonNameсериализация моделей c преобразованием

Темная сторона TypeScript — @декораторы на примерах - 11

Допустим, по каким-то причинам необходимо переименовать некоторые поля объекта при сериализации в JSON. С помощью декоратора мы сможем объявить JSON-имя поля, а после, при сериализации, его прочитать. Технически данный декоратор иллюстрирует работу библиотеки reflect-metadata, а, в частности, функций Reflect.defineMetadata и Reflect.getMetadata.

Реализация декоратора

// Уникальный ключ для наших метаданных
const JsonNameMetadataKey = "Habrahabr_PFight77_JsonName";
// Декоратор
function JsonName(name: string) {
    return (target: Object, propertyKey: string) => {
        // Сохраняем в метаданных переднный name
        Reflect.defineMetadata(JsonNameMetadataKey, name, target, propertyKey);
    }
}
// Функция, работающая в паре с декоратором
function serialize(model: Object): string {
  var result = {};
  var target = Object.getPrototypeOf(model);
  for(var prop in model) {
    // Загружаем сохраненное декоратором значение
    var jsonName = Reflect.getMetadata(JsonNameMetadataKey, target, prop) || prop;    
    result[jsonName] = model[prop];
  }
  return JSON.stringify(result);
}

class Model {
  @JsonName("name")
  public title: string;
}

var model = new Model();
model.title = "Hello there";
var json = serialize(model);
console.info(JSON.stringify(moel));
console.info(json);

Результат выполнения:

Темная сторона TypeScript — @декораторы на примерах - 12

» Попробовать в Plunker [39]
» Посмотреть в Playground [40]

Приведенный декоратор обладает тем недостатком, что, если модель содержит в качестве полей объекты других классов, то поля этих классов никак не обрабатываются методом serialize (то есть к ним нельзя применить декоратор @JsonName). Кроме того, здесь не реализовано обратное преобразование — из JSON в клиентскую модель. Оба этих недостатка исправлены в несколько более сложной реализации конвертера серверных моделей, в спойлере ниже.

@ServerModelField - конвертер серверных моделей на декораторах

@ServerModelField — конвертер серверных моделей на декораторах

Постановка задачи следующая. С сервера к нам прилетают некоторые JSON-данные примерно такого вида (похожий JSON шлет один BaaS сервис):

{
    "username":"PFight77",
    "email":"test@gmail.com",
    "doc": {
        "info":"The author of the article"
    }
}

Мы хотим конвертировать эти данные в типизированный объект, переименовывая некоторые поля. Выглядеть в конечном счете все будет так:

class UserAdditionalInfo {
    @ServerModelField("info")
    public mRole: string;
}
class UserInfo {
    @ServerModelField("username")
    private mUserName: string;
    @ServerModelField("email")
    private mEmail: string;
    @ServerModelField("doc")
    private mAdditionalInfo: UserAdditionalInfo;

    public get DisplayName() {
        return mUserName + " " + mAdditionalInfo.mRole;
    }
    public get ID() {
        return mEmail;
    }    
    public static parse(jsonData: string): UserInfo {
        return convertFromServer(JSON.parse(jsonData), UserInfo);
    }
    public serialize(): string {
        var serverData = convertToServer(this);
        return JSON.stringify(serverData);
    }
}

Разберем, как это реализовано.

Во-первых, нам необходимо определить декоратор поля [41] ServerModelField, который будет принимать строковый параметр и сохранять его в метаданных. Кроме того, для разбора JSON нам еще нужно знать, какие поля с нашим декоратором есть в классе вообще. Для этого объявим еще один экземпляр метаданных, общий для всех полей класса, в котором и сохраним имена всех декорированных членов. Здесь мы уже будем не только сохранять метаданные через Relect.defineMetadata, но и получать через Reflect.getMetadata.

// Объявляем уникальные ключи, по которым будем идентифицировать наши метаданные
const ServerNameMetadataKey = "Habrahabr_PFight77_ServerName";
const AvailableFieldsMetadataKey = "Habrahabr_PFight77_AvailableFields";
// Объявляем декоратор
export function ServerModelField(name?: string) {
    return (target: Object, propertyKey: string) => {
        // Сохраняем в метаданных переданный name, либо название самого свойства, если параметр не задан
        Reflect.defineMetadata(ServerNameMetadataKey, name || propertyKey, target, propertyKey);
        // Проверяем, не определены ли уже availableFields другим экземпляром декоратора
        var availableFields = Reflect.getMetadata(AvailableFieldsMetadataKey, target);
        if (!availableFields) {
            // Ok, мы первые, значит создаем новый массив
            availableFields = [];
            // Не передаем 4-й параметр(propertyKey) в defineMetadata, 
            // т.к. метаданные общие для всех полей
            Reflect.defineMetadata(AvailableFieldsMetadataKey, availableFields, target);            
        }
        // Регистрируем текущее поле в метаданных
        availableFields.push(propertyKey);
    }
}

Ну и осталось написать функцию convertFromServer. В ней почти нет ничего особенного, она просто вызывает Reflect.getMetadata и использует полученные метаданные для разбора JSON. Одна особенность — эта функция должна создать экземпляр UserInfo через new, поэтому мы передаем ей помимо JSON-данных еще и класс: convertFromServer(JSON.parse(data), UserInfo). Чтобы понять, как это работает, посмотрите спойлер ниже.

Передача класса параметром

class MyClass {
}
// Объявляем переменную типа "конструктор класса без параметров"
var myType: { new(): any; }; 
// Присваиваем переменной наш класс
myType = MyClass; 
// Эквивалентно new MyClass()
var obj = new myType();

Вторая особенность — это использование данных о типе поля, генерируемых благодаря настройке "emitDecoratorMetadata": true в tsconfig.json. Прием заключается в вызове Reflect.getMetadata с ключом "design:type", который возвращает конструктор соответствующего типа. Например, вызов Reflect.getMetadata("design:type", target, "mAdditionalInfo") вернет конструктор UserAdditionalInfo. Мы будем использовать эту информацию для того, чтобы правильно обрабатывать поля пользовательских типов. Например, класс UserAdditionalInfo также использует декоратор @ServerModelField, поэтому мы должны также использовать эти метаданные для анализа JSON.

Третья особенность заключается в получении соответствующего target, откуда мы будем брать метаданные. Мы используем декораторы полей [41], поэтому метаданные нужно брать из прототипа класса. Для декораторов статических членов нужно использовать конструктор класса. Получить прототип можно, вызвав Object.getPrototypeOf [42] или же обратившись к свойству prototype конструктора.

Все остальные комментарии в коде:

export function convertFromServer<T>(serverObj: Object, type: { new(): T ;} ): T {
    // Создаем объект, с помощью конструктора, переданного в параметре type
    var clientObj: T = new type();
    // Получаем контейнер с метаданными
    var target = Object.getPrototypeOf(clientObj);
    // Получаем из метаданных, какие декорированные свойства есть в классе
    var availableNames = Reflect.getMetadata(AvailableFieldsMetadataKey, target) as [string];
    if (availableNames) {
        // Обрабатываем каждое свойство
        availableNames.forEach(propName => {
            // Получаем из метаданных имя свойства в JSON
            var serverName = Reflect.getMetadata(ServerNameMetadataKey, target, propName);
            if (serverName) {
                // Получаем значение, переданное сервером
                var serverVal = serverObj[serverName];
                if (serverVal) {
                    var clientVal = null;
                    // Проверяем, используются ли в классе свойства декораторы @ServerModelField
                    // Получаем конструктор класса
                    var propType = Reflect.getMetadata("design:type", target, propName);
                    // Смотрим, есть ли в метаданных класса информация о свойствах
                    var propTypeServerFields =  Reflect.getMetadata(AvailableFieldsMetadataKey, propType.prototype) as [string];
                    if (propTypeServerFields) {
                        // Да, класс использует наш декоратор, обрабатываем свойство рекурсивно
                        clientVal = convertFromServer(serverVal, propType);
                    } else {
                        // Нет, просто копируем значение
                        clientVal = serverVal;
                    }
                    // Записываем результат в конечный объект
                    clientObj[propName] = clientVal;
                }
            }
        });
    } else {
        errorNoPropertiesFound(getTypeName(type));
    }

    return clientObj;
}
function errorNoPropertiesFound<T>(typeName: string) {
    throw new Error("There is no @ServerModelField directives in type '" + typeName + "'. Nothing to convert.");
}

function getTypeName<T>(type: { new(): T ;}) {
     return parseTypeName(type.toString());
}

function parseTypeName(ctorStr: string) {
     var matches = ctorStr.match(/w+/g);
     if (matches.length > 1) {
         return matches[1];
     } else {
         return "<can not determine type name>";

    }
}

Аналогичный вид имеет обратная функция — convertToServer.

Функция convertToServer

function convertToServer<T>(clientObj: T): Object {
    var serverObj = {};

    var target = Object.getPrototypeOf(clientObj);
    var availableNames = Reflect.getMetadata(AvailableFieldsMetadataKey, target) as [string];
    availableNames.forEach(propName=> {        
        var serverName = Reflect.getMetadata(ServerNameMetadataKey, target, propName);
        if (serverName) {
            var clientVal = clientObj[propName];
            if (clientVal) {
                var serverVal = null;
                var propType = Reflect.getMetadata("design:type", target, propName);
                var propTypeServerFields =  Reflect.getMetadata(AvailableFieldsMetadataKey, propType.prototype) as [string];
                if (clientVal && propTypeServerFields) {
                    serverVal = convertToServer(clientVal);
                } else {
                    serverVal = clientVal;
                }
                serverObj[serverName] = serverVal;
            }
        }
    });

    if (!availableNames) {
        errorNoPropertiesFound(parseTypeName(clientObj.constructor.toString()));
    }

    return serverObj;
}

Работу декоратора @ServerModelField в действии можно посмотреть в plunker [43].

@Controller, Action [44]сервисы для взаимодействия с сервером

Темная сторона TypeScript — @декораторы на примерах - 13

В ASP.NET сервер, как правило, состоит из контроллеров, которые содержат методы. Соответственно, url методов выглядит обычно, как /ControllerName/ActionName. В клиентском коде хорошей практикой будет сделать единую точку, через которую будут происходить все запросы к серверу вообще, и к каждому контроллеру в частности. Это позволит упросить рефакторинг, облегчит внедрение общей логики обработки ошибок и т.п.

С помощью декораторов можно красиво объявлять классы TypeScript, которые будут соответствовать контроллерам на сервере. Объявление методов при этом мы постараемся максимально упростить, так чтобы они содержали только одну строчку, а url будем формировать на основе информации из декораторов.

Реализация декоратора

var ControllerNameMetadataKey = "Habr_PFight77_ControllerName";
// Первый декоратор. 
// К сожалению, нет надежного способа узнать 
// имя класса (устойчивого к минификации),
// поэтому имя класса придется передавать вручную.
function Controller(name: string) {
    return (target: Function) {
    Reflect.defineMetadata(ControllerNameMetadataKey, name, target.prototype);
  };
}
// Второй декоратор, применяемый к методам
function Action(target: Object, propertyKey: string, descriptor: TypedPropertyDescriptor<any>): TypedPropertyDescriptor<any> {
  // Запоминаем исходную функцию
  var originalMethod = descriptor.value;
  // Подменяем ее на нашу обертку
  descriptor.value = function ActionWrapper () {
        // Получаем url, сохраненное декоратором Controller 
      var controllerName = Reflect.getMetadata(ControllerNameMetadataKey, target);
      // Формируем url вида /ControllerName/ActionName
      var url = "/" + controllerName + "/" + propertyKey;
      // Передаем url последним параметром
      [].push.call(arguments, url);
      // Вызываем исходный метод с дополнительным параметром
      originalMethod.apply(this, arguments);
  };
  // Обновляем дескриптор
  return descriptor;
}
// Функция, упрощающая объявление методов
function post(data: any, args: IArguments): any {
  // Получаем url, переданный декоратором @Action
  var url = args[args.length - 1];
  return $.ajax({ url: url, data: data, method: "POST" });
}

@Controller("Account")
class AccountController {
  @Action
  public Login(data: any): any {
    return post(data, arguments);
  }
}
var Account = new AccountController();
Account.Login({ username: "user", password: "111"});

Результат выполнения:

Темная сторона TypeScript — @декораторы на примерах - 14

» Попробовать в Plunker [45]
» Посмотреть в Playground [46]

Можно также добавить декораторы параметров так, чтобы сигнатура метода в TypeScript полностью повторяла сигнатуру серверного метода. С помощью декораторов можно сохранять имя каждого параметра и при выполнении запроса формировать на основе этих данных соответствующий JSON. К сожалению, получить имя параметра в коде декораторы не позволяют, поэтому придется передавать имя в декоратор вручную (так же, как в декоратор Controller [47]).

Заключение

Декораторы в TypeScript являются настолько мощным инструментом, что буквально позволяют расширять язык новой функциональностью. В этом плане они даже чем-то напоминают препроцессор в С++, с помощью которого можно здорово облагородить или запутать свой код.

Как было продемонстрировано в статье, в нашем распоряжении следующий ряд приемов:

  1. Модификация дескриптора метода или свойства. В частности, можно подменить метод оберткой, задать дескриптор для поля, с объявлением методов доступа и т.д. В целом, из декоратора можно произвести любую трансформацию прототипа класса.

  2. Сохранение и использование метаданных при помощи класса Reflect. Мы можем передать в декоратор любое значение, также нам доступно имя свойства или метода, устойчивое к минифкации.

  3. Получение информации о типе при помощи вызова Reflect.getMetada с ключом "design:type".

Использование этих приемов может быть самым разнообразным, в зависимости от конкретных нужд. Например, в Легком Клиенте 8 мы активно используем декораторы для объявления сервисов взаимодействия с сервером. Наша реализация чуть сложнее представленной в статье (мы используем декораторы параметров), но в целом построена по тому же принципу. Кроме того, мы думаем еще задействовать несколько декораторов для объявления публичного API наших ReactJS компонентов, а также автоматизировать привязку обработчиков событий к this [48].

На этом пока все. Пишите впечатления в комментариях, делитесь своим опытом использования декораторов.

UPD. Как заметил whileTrue [49], в ES7 декораторы не вошли. Будем надеяться, что хотя бы в ES8 попадут.

Темная сторона TypeScript — @декораторы на примерах - 15

Автор: ДоксВижн

Источник [50]


Сайт-источник PVSM.RU: https://www.pvsm.ru

Путь до страницы источника: https://www.pvsm.ru/javascript/200527

Ссылки в тексте:

[1] заявлены: https://github.com/tc39/proposal-decorators

[2] объектной модели JavaScript: https://habrahabr.ru/company/enterra/blog/153365/

[3] доступны в Babel: https://habrahabr.ru/post/277021/

[4] называют также свойствами: http://www.typescriptlang.org/docs/handbook/decorators.html#decorators

[5] появляется: http://www.typescriptlang.org/play/index.html#src=class%20MyClass%20%7B%0D%0A%09myField%3A%20number%3B%0D%0A%0D%0A%09get%20MyPrperty()%3A%20number%20%7B%0D%0A%09%09return%20this.myField%3B%0D%0A%09%7D%0D%0A%7D

[6] не появляется: http://www.typescriptlang.org/play/index.html#src=class%20MyClass%20%7B%0D%0A%09myField%3A%20number%3B%0D%0A%7D

[7] разного в каждом случае: #decorator-types

[8] TypeScript Playground: http://www.typescriptlang.org/play/

[9] Декоратор класса: http://www.typescriptlang.org/docs/handbook/decorators.html#class-decorators

[10] Пример в playground: http://www.typescriptlang.org/play/index.html#src=%40MyClassDecorator%0D%0Aclass%20MyClass%20%7B%0D%0A%09constructor()%20%7B%0D%0A%09%09console.info('it%20is%20ctor')%3B%0D%0A%09%7D%0D%0A%7D%0D%0A%0D%0Afunction%20MyClassDecorator(target%3A%20Function)%3A%20any%20%7B%0D%0A%09console.log(target)%3B%0D%0A%09return%20target%3B%0D%0A%7D)

[11] Декоратор метода: http://www.typescriptlang.org/docs/handbook/decorators.html#method-decorators

[12] Пример в playground: http://www.typescriptlang.org/play/index.html#src=class%20MyClass%20%7B%0D%0A%09%40MyMethodDecorator%0D%0A%09myMethod()%20%7B%0D%0A%09%7D%0D%0A%7D%0D%0A%0D%0Afunction%20MyMethodDecorator(target%3A%20Object%2C%20propertyKey%3A%20string%2C%20descriptor%3A%20TypedPropertyDescriptor%3Cany%3E)%3A%20TypedPropertyDescriptor%3Cany%3E%20%7B%0D%0A%09console.log(target)%3B%0D%0A%09console.log(propertyKey)%3B%0D%0A%09console.log(descriptor)%3B%0D%0A%09return%20descriptor%3B%0D%0A%7D

[13] дескриптор: https://developer.mozilla.org/en/docs/Web/JavaScript/Reference/Global_Objects/Object/defineProperty

[14] *: #target-es3-bug

[15] Пример в playground: http://www.typescriptlang.org/play/index.html#src=class%20MyClass%20%7B%0D%0A%09%40MyStaticMethodDecorator%0D%0A%09static%20myStaticMethod()%20%7B%20%7D%0D%0A%7D%0D%0A%0D%0Afunction%20MyStaticMethodDecorator(target%3A%20Object%2C%20propertyKey%3A%20string%2C%20descriptor%3A%20TypedPropertyDescriptor%3Cany%3E)%3A%20TypedPropertyDescriptor%3Cany%3E%20%7B%0D%0A%09console.log(target)%3B%0D%0A%09console.log(propertyKey)%3B%0D%0A%09console.log(descriptor)%3B%0D%0A%09return%20descriptor%3B%0D%0A%7D

[16] Декоратор методов доступа: http://www.typescriptlang.org/docs/handbook/decorators.html#accessor-decorators

[17] Пример в playground: http://www.typescriptlang.org/play/index.html#src=class%20MyClass%20%7B%0D%0A%09private%20mMyProperty%3A%20number%3B%0D%0A%0D%0A%09%40MyPropertyDecorator%0D%0A%09get%20myProperty()%3A%20number%20%7B%0D%0A%09%09return%20this.mMyProperty%3B%0D%0A%09%7D%0D%0A%09set%20myProperty(val%3A%20number)%20%7B%0D%0A%09%09this.mMyProperty%20%3D%20val%3B%0D%0A%09%7D%0D%0A%7D%0D%0A%0D%0Afunction%20MyPropertyDecorator(target%3A%20Object%2C%20propertyKey%3A%20string%2C%20descriptor%3A%20TypedPropertyDescriptor%3Cany%3E)%3A%20TypedPropertyDescriptor%3Cany%3E%20%7B%0D%0A%09console.log(target)%3B%0D%0A%09console.log(propertyKey)%3B%0D%0A%09console.log(descriptor)%3B%0D%0A%09return%20descriptor%3B%0D%0A%7D

[18] Декоратор параметра: http://www.typescriptlang.org/docs/handbook/decorators.html#parameter-decorators

[19] Пример в playground: http://www.typescriptlang.org/play/index.html#src=class%20MyClass%20%7B%0D%0A%09myMethod(%0D%0A%09%09%40MyParamDecorator%20param1%3A%20number%2C%0D%0A%09%09%40MyParamDecorator%20param2%3A%20number)%20%7B%0D%0A%09%7D%0D%0A%7D%0D%0A%0D%0Afunction%20MyParamDecorator(target%3A%20Object%2C%20propertyKey%3A%20string%2C%20index%3A%20number)%20%7B%0D%0A%09console.log(target)%3B%0D%0A%09console.log(propertyKey)%3B%0D%0A%09console.log(index)%3B%0D%0A%7D

[20] Декоратор поля (свойства): http://www.typescriptlang.org/docs/handbook/decorators.html#property-decorators

[21] Пример в playground: http://www.typescriptlang.org/play/index.html#src=class%20MyClass%20%7B%0D%0A%09%40MyFieldDecorator%0D%0A%09myField%3A%20number%3B%0D%0A%7D%0D%0A%0D%0Afunction%20MyFieldDecorator(target%3A%20Object%2C%20propertyKey%3A%20string)%20%7B%0D%0A%09console.log(target)%3B%0D%0A%09console.log(propertyKey)%3B%0D%0A%7D

[22] баг в reflect-metadata: https://github.com/rbuckton/ReflectDecorators/issues/48

[23] Пример в playground: http://www.typescriptlang.org/play/index.html#src=class%20MyClass%20%7B%0D%0A%09%40MyFieldDecorator%0D%0A%09static%20myField%3A%20number%3B%0D%0A%7D%0D%0A%0D%0Afunction%20MyFieldDecorator(target%3A%20Function%2C%20propertyKey%3A%20string)%20%7B%0D%0A%09console.log(target)%3B%0D%0A%09console.log(propertyKey)%3B%0D%0A%7D

[24] TypeScript 2.0: http://www.typescriptlang.org/docs/release-notes/typescript-2.0.html

[25] это баг в компиляторе TypeScript: https://github.com/Microsoft/TypeScript/issues/11658

[26] баг в JSFiddle: https://github.com/jsfiddle/jsfiddle-issues/issues/906

[27] реализация декоратора @safe: https://jsfiddle.net/PFight/msac244r/

[28] Reflect: https://www.npmjs.com/package/reflect-metadata#api

[29] reflect-metadata: https://github.com/rbuckton/ReflectDecorators

[30] typings: https://github.com/DefinitelyTyped/DefinitelyTyped/tree/master/reflect-metadata

[31] Попробовать в действии в Plunker: https://embed.plnkr.co/hJIvnIYCdmnvJlUZZUCw/

[32] Посмотреть в Playground: http://www.typescriptlang.org/play/index.html#src=class%20MyClass%20%7B%0D%0A%09%0D%0A%09%40safe%20public%20foo(str%3A%20string%2C%20data%3A%20any)%3A%20boolean%20%7B%0D%0A%20%20%20%20return%20str.length%20%3E%200%3B%0D%0A%20%20%7D%0D%0A%7D%0D%0A%0D%0Afunction%20safe(target%3A%20Object%2C%20propertyKey%3A%20string)%20%7B%0D%0A%20%20var%20descriptor%20%3D%20Object.getOwnPropertyDescriptor(target%2C%20propertyKey)%3B%0D%0A%20%20var%20originalMethod%20%3D%20descriptor.value%3B%0D%0A%20%20descriptor.value%20%3D%20function%20NoErrorWrapper%20()%20%7B%0D%0A%20%20%20%20try%20%7B%0D%0A%20%20%20%20%20%20originalMethod.apply(this%2C%20arguments)%3B%0D%0A%20%20%20%20%7D%20catch(ex)%20%7B%0D%0A%20%20%20%20%20%20var%20message%20%3D%20ex.name%20%2B%20%22%20occured%20in%20call%20of%20%22%20%2B%20%0D%0A%20%20%20%20%20%20%20%20%20%20target.constructor.name%20%2B%20%22.%22%20%2B%20propertyKey%20%2B%20%22%3A%20%22%20%2B%20ex.message%20%2B%20%22%5Cn%22%3B%0D%0A%20%20%20%20%20%20message%20%2B%3D%20%22Arguments%3A%5Cn%22%3B%0D%0A%20%20%20%20%20%20%5B%5D.forEach.call(arguments%2C%20(x%2C%20i)%20%3D%3E%20%0D%0A%20%20%20%20%20%20%20%20%20message%20%2B%3D%20%22%20%20%20%5B%22%20%2B%20i%20%2B%20%22%5D%3A%20%22%20%2B%20JSON.stringify(x)%20%2B%20%22%20%5Cn%22)%3B%0D%0A%20%20%20%20%20%20message%20%2B%3D%20%22Callstack%3A%22%20%2B%20ex.stack%3B%0D%0A%20%20%20%20%20%20console.error(message)%3B%0D%0A%20%20%20%20%20%20document.body.innerHTML%20%2B%3D%20%22%3Cbr%3E%3Cpre%20style%3D'color%3A%20red'%3E%22%20%2B%20message%20%2B%20%22%3C%2Fpre%3E%22%3B%0D%0A%20%20%20%20%7D%0D%0A%20%20%7D%3B%0D%0A%20%20Object.defineProperty(target%2C%20propertyKey%2C%20descriptor)%3B%0D%0A%20%20return%20descriptor%3B%0D%0A%7D%0D%0A%0D%0Aconsole.info(%22Starting...%22)%3B%0D%0Adocument.body.innerHTML%20%2B%3D%20%22Starting...%3Cbr%3E%22%3B%0D%0Avar%20my%20%3D%20new%20MyClass()%3B%0D%0Amy.foo(null%2C%20%7B%20answer%3A%2042%20%7D)%3B%0D%0Aconsole.info(%22Continue%20execution%22)%3B%0D%0Adocument.body.innerHTML%20%2B%3D%20%22%3Cbr%3EContinue%20execution%22%3B

[33] Попробовать в действии в Plunker: https://embed.plnkr.co/7k6FZqoB9gYHm0yYqiXC/

[34] Посмотреть в Playground: http://www.typescriptlang.org/play/index.html#src=class%20MyClass%20%7B%0D%0A%09%40OnChange(MyClass.onFieldChange)%0D%0A%09public%20mMyField%3A%20number%20%3D%2042%3B%0D%0A%0D%0A%09static%20onFieldChange(self%3A%20MyClass%2C%20newVal%3A%20number)%3A%20void%20%7B%0D%0A%20%20%20%20document.body.innerHTML%20%2B%3D%0D%0A%20%20%20%20%20%20%22%3Cli%3EChanging%20from%20%22%20%2B%20self.mMyField%20%2B%20%22%20to%20%22%20%2B%20newVal%20%2B%20%22%3C%2Fli%3E%22%3B%0D%0A%09%7D%0D%0A%7D%0D%0A%0D%0Afunction%20OnChange%3CClassT%2C%20T%3E(callback%3A%20(ClassT%2C%20T)%20%3D%3E%20void)%3A%20any%20%7B%0D%0A%20%20%20%20return%20(target%3A%20Object%2C%20propertyKey%3A%20string%20%7C%20symbol)%20%3D%3E%20%7B%0D%0A%20%20%20%20%20%20%2F%2F%20%D0%9D%D0%B5%D0%BE%D0%B1%D1%85%D0%BE%D0%B4%D0%B8%D0%BC%D0%BE%20%D0%B7%D0%B0%D0%B4%D0%B5%D0%B9%D1%81%D1%82%D0%B2%D0%BE%D0%B2%D0%B0%D1%82%D1%8C%20%D1%81%D1%83%D1%89%D0%B5%D1%81%D1%82%D0%B2%D1%83%D1%8E%D1%89%D0%B8%D0%B9%20%D0%B4%D0%B5%D1%81%D0%BA%D1%80%D0%B8%D0%BF%D1%82%D0%BE%D1%80%2C%20%D0%B5%D1%81%D0%BB%D0%B8%20%D0%BE%D0%BD%20%D0%B5%D1%81%D1%82%D1%8C.%0D%0A%20%20%20%20%20%20%2F%2F%20%D0%AD%D1%82%D0%BE%20%D0%BF%D0%BE%D0%B7%D0%B2%D0%BE%D0%BB%D0%B8%D1%82%20%D0%BE%D0%B1%D1%8A%D1%8F%D0%B2%D1%8F%D1%82%D1%8C%20%D0%BD%D0%B5%D1%81%D0%BA%D0%BE%D0%BB%D1%8C%D0%BA%D0%BE%20%D0%B4%D0%B5%D0%BA%D0%BE%D1%80%D0%B0%D1%82%D0%BE%D1%80%D0%BE%D0%B2%20%D0%BD%D0%B0%20%D0%BE%D0%B4%D0%BD%D0%BE%D0%BC%20%D1%81%D0%B2%D0%BE%D0%B9%D1%81%D1%82%D0%B2%D0%B5.%0D%0A%20%20%20%20%20%20var%20descriptor%20%3D%20Object.getOwnPropertyDescriptor(target%2C%20propertyKey)%20%0D%0A%20%20%20%20%20%20%20%20%7C%7C%20%7Bconfigurable%3A%20true%2C%20enumerable%3A%20true%7D%3B%0D%0A%20%20%20%20%20%20%2F%2F%20%D0%9F%D0%BE%D0%B4%D0%BC%D0%B5%D0%BD%D1%8F%D0%B5%D0%BC%20%D0%B8%D0%BB%D0%B8%20%D0%BE%D0%B1%D1%8A%D1%8F%D0%B2%D0%BB%D1%8F%D0%B5%D0%BC%20get%20%D0%B8%20set%0D%0A%20%20%20%20%20%20var%20value%3A%20T%3B%0D%0A%20%20%20%20%20%20var%20originalGet%20%3D%20descriptor.get%20%7C%7C%20(()%20%3D%3E%20value)%3B%0D%0A%20%20%20%20%20%20var%20originalSet%20%3D%20descriptor.set%20%7C%7C%20(val%20%3D%3E%20value%20%3D%20val)%3B%0D%0A%20%20%20%20%20%20descriptor.get%20%3D%20originalGet%3B%0D%0A%20%20%20%20%20%20descriptor.set%20%3D%20function(newVal%3A%20T)%20%7B%0D%0A%20%20%20%20%20%20%20%20%2F%2F%20%D0%92%D0%BD%D0%B8%D0%BC%D0%B0%D0%BD%D0%B8%D0%B5%2C%20%D0%B5%D1%81%D0%BB%D0%B8%20%D0%BE%D0%BF%D1%80%D0%B5%D0%B4%D0%B5%D0%BB%D1%8F%D0%B5%D0%BC%20set%20%D1%87%D0%B5%D1%80%D0%B5%D0%B7%20function%2C%20%0D%0A%20%20%20%20%20%20%20%20%2F%2F%20%D1%82%D0%BE%20this%20-%20%D1%82%D0%B5%D0%BA%D1%83%D1%89%D0%B8%D0%B9%20%D1%8D%D0%BA%D0%B7%D0%B5%D0%BC%D0%BF%D0%BB%D1%8F%D1%80%20%D0%BA%D0%BB%D0%B0%D1%81%D1%81%D0%B0%2C%0D%0A%20%20%20%20%20%20%20%20%2F%2F%20%D0%B5%D1%81%D0%BB%D0%B8%20%D1%87%D0%B5%D1%80%D0%B5%D0%B7%20%D0%BB%D1%8F%D0%BC%D0%B1%D0%B4%D1%83%2C%20%D1%82%D0%BE%20this%20-%20Window!!!%0D%0A%20%20%20%20%20%20%20%20var%20currentVal%20%3D%20originalGet.call(this)%3B%0D%0A%20%20%20%20%20%20%20%20if%20(newVal%20!%3D%20currentVal)%20%7B%0D%0A%20%20%20%20%20%20%20%20%09%2F%2F%20%D0%92%D1%8B%D0%B7%D1%8B%D0%B2%D0%B0%D0%B5%D0%BC%20%D1%81%D1%82%D0%B0%D1%82%D0%B8%D1%87%D0%B5%D1%81%D0%BA%D0%B8%D0%B9%20%D0%BC%D0%B5%D1%82%D0%BE%D0%B4%20callback%20%D1%81%20%D0%B4%D0%B2%D1%83%D0%BC%D1%8F%20%D0%BF%D0%B0%D1%80%D0%B0%D0%BC%D0%B5%D1%82%D1%80%D0%B0%D0%BC%D0%B8%0D%0A%20%20%20%20%20%20%20%20%20%20callback.call(target.constructor%2C%20this%2C%20newVal)%3B%0D%0A%20%20%20%20%20%20%20%20%7D%0D%0A%20%20%20%20%20%20%20%20originalSet.call(this%2C%20newVal)%3B%0D%0A%20%20%20%20%20%20%7D%3B%0D%0A%20%20%20%20%20%20%2F%2F%20%D0%9E%D0%B1%D1%8A%D1%8F%D0%B2%D0%BB%D1%8F%D0%B5%D0%BC%20%D0%BD%D0%BE%D0%B2%D0%BE%D0%B5%20%D1%81%D0%B2%D0%BE%D0%B9%D1%81%D1%82%D0%B2%D0%BE%2C%20%D0%BB%D0%B8%D0%B1%D0%BE%20%D0%BE%D0%B1%D0%BD%D0%BE%D0%B2%D0%BB%D1%8F%D0%B5%D0%BC%20%D0%B4%D0%B5%D1%81%D0%BA%D1%80%D0%B8%D0%BF%D1%82%D0%BE%D1%80%0D%0A%20%20%20%20%20%20Object.defineProperty(target%2C%20propertyKey%2C%20descriptor)%3B%0D%0A%20%20%20%20%7D%0D%0A%7D%0D%0A%0D%0Avar%20my%20%3D%20new%20MyClass()%3B%0D%0Amy.mMyField%20%3D%2043%3B%0D%0Amy.mMyField%20%3D%2044%3B

[35] альтернативный вариант со строковым параметром: https://jsfiddle.net/PFight/p7ubjx6c

[36] с использованием лямбды: https://jsfiddle.net/PFight/525kjkgp/1/

[37] Попробовать в Plunker: https://embed.plnkr.co/fcjDyqdK6kG9yPWlTsNB/

[38] Посмотреть в Playground: http://www.typescriptlang.org/play/index.html#src=%0D%0A%0D%0Afunction%20Inject(target%3A%20Object%2C%20propKey%3A%20string)%3A%20any%20%7B%0D%0A%09%09var%20propType%20%3D%20Reflect.getMetadata(%22design%3Atype%22%2C%20target%2C%20propKey)%3B%0D%0A%20%20%20%09console.log(propType)%3B%0D%0A%20%20%20%20var%20descriptor%20%3D%20%7B%0D%0A%20%20%20%20%09get%3A%20function%20()%20%7B%20%0D%0A%20%20%20%20%09%20%20var%20serviceLocator%20%3D%20this.serviceLocator%20%7C%7C%20globalSericeLocator%3B%0D%0A%20%20%20%20%09%20%20return%20%20serviceLocator.getService(propType)%3B%20%20%0D%0A%20%20%20%20%09%20%20%0D%0A%20%20%20%20%09%7D%0D%0A%20%20%20%20%7D%3B%0D%0A%20%20%20%20Object.defineProperty(target%2C%20propKey%2C%20descriptor)%3B%0D%0A%20%20%20%20return%20descriptor%3B%0D%0A%7D%0D%0A%0D%0Aclass%20MyClass%20%7B%0D%0A%09%40Inject%0D%0A%20%20private%20logService%3A%20ILogService%3B%0D%0A%20%20%0D%0A%20%20sayHello()%20%7B%0D%0A%20%20%09this.logService.log(%22Hello%20there%22)%3B%0D%0A%20%20%7D%0D%0A%7D%0D%0A%0D%0A%0D%0Aabstract%20class%20ILogService%20%7B%0D%0A%09abstract%20log(msg%3A%20string)%3A%20void%3B%0D%0A%7D%0D%0A%0D%0Aclass%20ServiceLocator%20%7B%0D%0A%20%20services%3A%20%5B%7BinterfaceType%3A%20Function%2C%20instance%3A%20Object%20%7D%5D%20%3D%20%5B%5D%20as%20any%3B%0D%0A%0D%0A%20%20registerService(interfaceType%3A%20Function%2C%20instance%3A%20Object)%20%7B%0D%0A%20%20%20%20var%20record%20%3D%20this.services.find(x%20%3D%3E%20x.interfaceType%20%3D%3D%20interfaceType)%3B%0D%0A%20%20%20%20if%20(!record)%20%7B%0D%0A%20%20%20%20%20%20record%20%3D%20%7B%20interfaceType%3A%20interfaceType%2C%20instance%3A%20instance%7D%3B%0D%0A%20%20%20%20%20%20this.services.push(record)%3B%0D%0A%20%20%20%20%7D%20else%20%7B%0D%0A%20%20%20%20%20%20record.instance%20%3D%20instance%3B%0D%0A%20%20%20%20%7D%0D%0A%20%20%7D%0D%0A%20%20getService(interfaceType%3A%20Function)%20%7B%0D%0A%20%20%20%20return%20this.services.find(x%20%3D%3E%20x.interfaceType%20%3D%3D%20interfaceType).instance%3B%0D%0A%20%20%7D%0D%0A%7D%0D%0Avar%20globalSericeLocator%20%3D%20new%20ServiceLocator()%3B

[39] Попробовать в Plunker: https://embed.plnkr.co/q0PJWcLMnMUepaDbRNjb/

[40] Посмотреть в Playground: https://www.typescriptlang.org/play/index.html#src=%2F%2F%20%D0%A3%D0%BD%D0%B8%D0%BA%D0%B0%D0%BB%D1%8C%D0%BD%D1%8B%D0%B9%20%D0%BA%D0%BB%D1%8E%D1%87%20%D0%B4%D0%BB%D1%8F%20%D0%BD%D0%B0%D1%88%D0%B8%D1%85%20%D0%BC%D0%B5%D1%82%D0%B0%D0%B4%D0%B0%D0%BD%D0%BD%D1%8B%D1%85%0D%0Aconst%20JsonNameMetadataKey%20%3D%20%22Habrahabr_PFight77_JsonName%22%3B%0D%0A%2F%2F%20%D0%94%D0%B5%D0%BA%D0%BE%D1%80%D0%B0%D1%82%D0%BE%D1%80%0D%0Afunction%20JsonName(name%3A%20string)%20%7B%0D%0A%20%20%20%20return%20(target%3A%20Object%2C%20propertyKey%3A%20string)%20%3D%3E%20%7B%0D%0A%20%20%20%20%20%20%20%20%2F%2F%20%D0%A1%D0%BE%D1%85%D1%80%D0%B0%D0%BD%D1%8F%D0%B5%D0%BC%20%D0%B2%20%D0%BC%D0%B5%D1%82%D0%B0%D0%B4%D0%B0%D0%BD%D0%BD%D1%8B%D1%85%20%D0%BF%D0%B5%D1%80%D0%B5%D0%B4%D0%BD%D0%BD%D1%8B%D0%B9%20name%0D%0A%20%20%20%20%20%20%20%20Reflect.defineMetadata(JsonNameMetadataKey%2C%20name%2C%20target%2C%20propertyKey)%3B%0D%0A%20%20%20%20%7D%0D%0A%7D%0D%0Afunction%20serialize(model%3A%20Object)%3A%20string%20%7B%0D%0A%09var%20result%20%3D%20%7B%7D%3B%0D%0A%20%20var%20target%20%3D%20Object.getPrototypeOf(model)%3B%0D%0A%20%20for(var%20prop%20in%20model)%20%7B%0D%0A%20%20%09%2F%2F%20%D0%97%D0%B0%D0%B3%D1%80%D1%83%D0%B6%D0%B0%D0%B5%D0%BC%20%D1%81%D0%BE%D1%85%D1%80%D0%B0%D0%BD%D0%B5%D0%BD%D0%BD%D0%BE%D0%B5%20%D0%B4%D0%B5%D0%BA%D0%BE%D1%80%D0%B0%D1%82%D0%BE%D1%80%D0%BE%D0%BC%20%D0%B7%D0%BD%D0%B0%D1%87%D0%B5%D0%BD%D0%B8%D0%B5%0D%0A%20%20%09var%20jsonName%20%3D%20Reflect.getMetadata(%0D%0A%20%20%20%20%09JsonNameMetadataKey%2C%20target%2C%20prop)%20%7C%7C%20prop%3B%20%20%20%20%0D%0A%20%20%20%20result%5BjsonName%5D%20%3D%20model%5Bprop%5D%3B%0D%0A%20%20%7D%0D%0A%20%20return%20JSON.stringify(result)%3B%0D%0A%7D%0D%0A%0D%0Aclass%20Model%20%7B%0D%0A%20%20%40JsonName(%22name%22)%0D%0A%20%20public%20title%3A%20string%3B%0D%0A%20%20public%20description%3A%20string%3B%0D%0A%7D%0D%0A%0D%0Avar%20model%20%3D%20new%20Model()%3B%0D%0Amodel.title%20%3D%20%22Hello%20there%22%3B%0D%0Amodel.description%20%3D%20%22Decorators%20are%20cool!%22%0D%0Avar%20json%20%3D%20serialize(model)%3B%0D%0Adocument.body.innerHTML%20%2B%3D%20%22%3Ch3%3EModel%3A%3C%2Fh3%3E%3Cp%3E%3Cpre%3E%22%20%2B%20JSON.stringify(model)%20%2B%20%22%3C%2Fpre%3E%3C%2Fp%3E%22%3B%0D%0Adocument.body.innerHTML%20%2B%3D%20%22%3Ch3%3EResult%20json%3A%3C%2Fh3%3E%3Cp%3E%3Cpre%3E%22%20%2B%20json%20%2B%20%22%3C%2Fpre%3E%3C%2Fp%3E%22%3B%0D%0A

[41] декоратор поля: #field-decorator

[42] Object.getPrototypeOf: https://developer.mozilla.org/ru/docs/Web/JavaScript/Reference/Global_Objects/Object/getPrototypeOf

[43] посмотреть в plunker: http://embed.plnkr.co/MJLm7v

[44] Action: https://habrahabr.ru/users/action/

[45] Попробовать в Plunker: https://embed.plnkr.co/vLwbHJOG2kWwEjbHDrLl/

[46] Посмотреть в Playground: https://www.typescriptlang.org/play/index.html#src=var%20ControllerNameMetadataKey%20%3D%20%22Habr_PFight77_ControllerName%22%3B%0D%0Afunction%20Controller(name%3A%20string)%20%7B%0D%0A%09return%20(target%3A%20Function)%20%7B%0D%0A%20%20%20%20Reflect.defineMetadata(ControllerNameMetadataKey%2C%20name%2C%20target.prototype)%3B%0D%0A%20%20%7D%3B%0D%0A%7D%0D%0Afunction%20Action(target%3A%20Object%2C%20propertyKey%3A%20string)%20%7B%20%20%0D%0A%20%20%2F%2F%20%D0%9F%D0%BE%D0%BB%D1%83%D1%87%D0%B0%D0%B5%D0%BC%20%D1%82%D0%B5%D0%BA%D1%83%D1%89%D0%B8%D0%B9%20%D0%B4%D0%B5%D1%81%D0%BA%D1%80%D0%B8%D0%BF%D1%82%D0%BE%D1%80%20%D0%BC%D0%B5%D1%82%D0%BE%D0%B4%D0%B0%0D%0A%20%20var%20descriptor%20%3D%20Object.getOwnPropertyDescriptor(target%2C%20propertyKey)%3B%0D%0A%20%20%2F%2F%20%D0%97%D0%B0%D0%BF%D0%BE%D0%BC%D0%B8%D0%BD%D0%B0%D0%B5%D0%BC%20%D0%B8%D1%81%D1%85%D0%BE%D0%B4%D0%BD%D1%83%D1%8E%20%D1%84%D1%83%D0%BD%D0%BA%D1%86%D0%B8%D1%8E%0D%0A%20%20var%20originalMethod%20%3D%20descriptor.value%3B%0D%0A%20%20%2F%2F%20%D0%9F%D0%BE%D0%B4%D0%BC%D0%B5%D0%BD%D1%8F%D0%B5%D0%BC%20%D0%B5%D0%B5%20%D0%BD%D0%B0%20%D0%BD%D0%B0%D1%88%D1%83%20%D0%BE%D0%B1%D0%B5%D1%80%D1%82%D0%BA%D1%83%0D%0A%20%20descriptor.value%20%3D%20function%20ActionWrapper%20()%20%7B%0D%0A%20%20%09%09%2F%2F%20%D0%9F%D0%BE%D0%BB%D1%83%D1%87%D0%B0%D0%B5%D0%BC%20url%2C%20%D1%81%D0%BE%D1%85%D1%80%D0%B0%D0%BD%D0%B5%D0%BD%D0%BD%D0%BE%D0%B5%20%D0%B4%D0%B5%D0%BA%D0%BE%D1%80%D0%B0%D1%82%D0%BE%D1%80%D0%BE%D0%BC%20Controller%20%0D%0A%20%20%20%20%20%20var%20controllerName%20%3D%20Reflect.getMetadata(ControllerNameMetadataKey%2C%20target)%3B%0D%0A%20%20%20%20%20%20var%20url%20%3D%20%22%2F%22%20%2B%20controllerName%20%2B%20%22%2F%22%20%2B%20propertyKey%3B%0D%0A%20%20%09%09%2F%2F%20%D0%9F%D0%B5%D1%80%D0%B5%D0%B4%D0%B0%D0%B5%D0%BC%20url%20%D0%BF%D0%BE%D1%81%D0%BB%D0%B5%D0%B4%D0%BD%D0%B8%D0%BC%20%D0%BF%D0%B0%D1%80%D0%B0%D0%BC%D0%B5%D1%82%D1%80%D0%BE%D0%BC%0D%0A%20%20%09%09%5B%5D.push.call(arguments%2C%20url)%3B%0D%0A%20%20%20%20%20%20%2F%2F%20%D0%92%D1%8B%D0%B7%D1%8B%D0%B2%D0%B0%D0%B5%D0%BC%20%D0%B8%D1%81%D1%85%D0%BE%D0%B4%D0%BD%D1%8B%D0%B9%20%D0%BC%D0%B5%D1%82%D0%BE%D0%B4%0D%0A%20%20%20%20%20%20originalMethod.apply(this%2C%20arguments)%3B%0D%0A%20%20%7D%3B%0D%0A%20%20%2F%2F%20%D0%9E%D0%B1%D0%BD%D0%BE%D0%B2%D0%BB%D1%8F%D0%B5%D0%BC%20%D0%B4%D0%B5%D1%81%D0%BA%D1%80%D0%B8%D0%BF%D1%82%D0%BE%D1%80%0D%0A%20%20Object.defineProperty(target%2C%20propertyKey%2C%20descriptor)%3B%0D%0A%20%20return%20descriptor%3B%0D%0A%7D%0D%0Afunction%20post(data%3A%20any%2C%20args%3A%20IArguments)%3A%20any%20%7B%0D%0A%09var%20url%20%3D%20args%5Bargs.length%20-%201%5D%3B%0D%0A%20%20document.body.innerHTML%20%3D%20%22Performing%20request%20to%20url%3A%20%22%20%2B%20url%20%2B%20%0D%0A%20%20%09%22%20%3Cbr%3EData%3A%20%22%20%2B%20JSON.stringify(data)%3B%0D%0A%20%20return%20%24.ajax(%7B%20url%3A%20url%2C%20data%3A%20data%2C%20method%3A%20%22POST%22%20%7D)%3B%0D%0A%7D%0D%0A%0D%0A%40Controller(%22Account%22)%0D%0Aclass%20AccountController%20%7B%0D%0A%09%40Action%0D%0A%20%20public%20Login(data%3A%20any)%3A%20any%20%7B%0D%0A%20%20%20%20return%20post(data%2C%20arguments)%3B%0D%0A%20%20%7D%0D%0A%7D%0D%0Avar%20Account%20%3D%20new%20AccountController()%3B%0D%0A%0D%0AAccount.Login(%7B%20username%3A%20%22user%22%2C%20password%3A%20%22111%22%7D)%3B

[47] Controller: https://habrahabr.ru/users/controller/

[48] привязку обработчиков событий к this: http://stackoverflow.com/questions/29732015/value-of-this-in-react-event-handler

[49] whileTrue: https://habrahabr.ru/users/whiletrue/

[50] Источник: https://habrahabr.ru/post/310870/?utm_source=habrahabr&utm_medium=rss&utm_campaign=best