67,39
рейтинг
13 января в 12:01

Разработка → Декораторы и рефлексия в TypeScript: от новичка до эксперта (ч.1) перевод



От переводчика: TypeScript — довольно молодой и активно развивающийся язык. К сожалению, в русскоязычной части Интернета о нем довольно мало информации, что не способствует его популярности.

Многие возможности, которые сейчас реализованы в ES6, значительно раньше появились именно в TypeScript. Более того, некоторые возможности и предложенные стандарты ES7 также имеют экспериментальную реализацию в этом языке. Об одной из них, появившейся сравнительно недавно — декораторах — и пойдет речь.

Предлагаю вашему вниманию перевод статьи (а точнее, цикла статей) о декораторах в TypeScript под авторством Remo H.Jansen



Не так давно Microsoft и Google объявили о совместной работе над TypeScript и Angular 2.0.

Мы рады объявить, что мы объединяем языки TypeScript и AtScript, а также, что Angular 2, следующая версия популярной JavaScript-библиотеки для создания сайтов и веб-приложений, будет разработана на TypeScript





Аннотации и декораторы



Это сотрудничество помогло TypeScript развить новые возможности языка, среди которых мы выделим аннотации.

Аннотации — способ добавления метаданных к объявлению класса для использования во внедрении зависимостей или директивах компилятора.



Аннотации были предложены командой AtScript из Google, но они не явяются стандартом. Тем временем, декораторы были предложены в качестве стандарта в ECMAScript 7 для изменения классов и свойств на этапе разработки.

Аннотации и декораторы очень похожи:

Аннотации и декораторы — это примерно одно и то же. С точки зрения пользователя, у них абсолютно одинаковый синтаксис. Отличие в том, что мы не контролируем то, как аннотации добавляют метаданные в наш код. Мы можем рассматривать декораторы как интерфейс для построения чего-то, что ведет себя как аннотации.

В долгосрочной перспективе, однако, мы сфокусируемся на декораторах, так как они — существующий предложенный стандарт. AtScript — это TypeScript, а TypeScript реализует декораторы.



Давайте посмотрим на синтаксис декораторов в TypeScript.

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



В исходном коде TypeScript можно найти сигнатуры доступных типов декораторов:

declare type ClassDecorator = <TFunction extends Function>(target: TFunction) => TFunction | void;

declare type PropertyDecorator = (target: Object, propertyKey: string | symbol) => void;

declare type MethodDecorator = <T>(target: Object, propertyKey: string | symbol, descriptor: TypedPropertyDescriptor<T>) => TypedPropertyDescriptor<T> | void;

declare type ParameterDecorator = (target: Object, propertyKey: string | symbol, parameterIndex: number) => void;

Как можно заметить, они могут быть использованы для аннотации класса, свойства, метода или параметра. Давайте подробно рассмотрим каждый из этих типов.

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



Теперь, когда мы знаем, как выглядит сигнатура декораторов, мы можем попробовать их реализовать. Начнем с декоратора метода. Давайте создадим декоратор метода под названием "log".

Для вызова декоратора перед объявлением метода нужно написать символ "@", а затем название используемого декоратора. В случае, если декоратор называется "log", этот синтаксис выглядит так:

class C {
    @log
    foo(n: number) {
        return n * 2;
    }
}

Перед тем, как использовать @log, необходимо объявить сам декоратор где-либо в нашем приложении. Давайте посмотрим на его реализацию:

function log(target: Function, key: string, value: any) {
    return {
        value: function (...args: any[]) {
            var a = args.map(a => JSON.stringify(a)).join();
            var result = value.value.apply(this, args);
            var r = JSON.stringify(result);
            console.log(`Call: ${key}(${a}) => ${r}`);
            return result;
        }
    };
}

Декоратор метода принимает 3 аргумента:

  • target — метод, для которого применяется декоратор;
  • key — имя этого метода;
  • value — дескриптор данного свойства (property descriptor), если оно существует в рамках объекта, иначе undefined. Получить дескриптор можно при помощи метода Object.getOwnPropertyDescriptor().


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

Ответы на них можно найти, посмотрев в код, который сгенерирует TypeScript для примера выше.

   var C = (function () {
    function C() {
    }
    C.prototype.foo = function (n) {
        return n * 2;
    };
    Object.defineProperty(C.prototype, "foo",
        __decorate([
            log
        ], C.prototype, "foo", Object.getOwnPropertyDescriptor(C.prototype, "foo")));
    return C;
})();

Без декоратора @log сгенерированный JavaScript-код для класса C будет выглядеть так:

   var C = (function () {
    function C() {
    }
    C.prototype.foo = function (n) {
        return n * 2;
    };
    return C;
})();

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

Object.defineProperty(C.prototype, "foo",
    __decorate(
        [log],                                              // decorators
        C.prototype,                                        // target
        "foo",                                              // key
        Object.getOwnPropertyDescriptor(C.prototype, "foo") // desc
    );
  );

Если мы обратимся к документации, то узнаем следующее про метод defineProperty:

Метод Object.defineProperty() определяет новое или изменяет существующее свойство прямо на объекте и возвращает этот объект.



Компилятор TypeScript передает прототип C, название декорируемого метода ('foo') и результат выполнения функции __decorate в метод defineProperty.

defineProperty используется для того, чтобы переопределить декорируемый метод. Новая реализация метода представляет собой результат работы функции __decorate. Возникает новый вопрос: где объявлена функция __decorate?

Если вы до этого работали с TypeScript, то могли заметить, что когда мы используем ключевое слово extends, функция под названием __extends генерируется компилятором.

var __extends = this.__extends || function (d, b) {
    for (var p in b) if (b.hasOwnProperty(p)) d[p] = b[p];
    function __() { this.constructor = d; }
    __.prototype = b.prototype;
    d.prototype = new __();
};

Аналогично, когда мы используем декоратор, функция под названием __decorate генерируется компилятором TypeScript. Давайте на нее посмотрим.

var __decorate = this.__decorate || function (decorators, target, key, desc) {
    if (typeof Reflect === "object" && typeof Reflect.decorate === "function") {
        return Reflect.decorate(decorators, target, key, desc);
    }
    switch (arguments.length) {
        case 2: 
            return decorators.reduceRight(function(o, d) { 
                return (d && d(o)) || o; 
            }, target);
        case 3: 
            return decorators.reduceRight(function(o, d) { 
                return (d && d(target, key)), void 0; 
            }, void 0);
        case 4: 
            return decorators.reduceRight(function(o, d) { 
                return (d && d(target, key, o)) || o; 
            }, desc);
    }
};

Первая строчка в этом листинге использует оператор OR чтобы убедиться, что функция __decorate, сгенерированная более одного раза, не будет перезаписываться снова и снова. Во второй строчке мы можем заметить условие:

if (typeof Reflect === "object" && typeof Reflect.decorate === "function")

Это условие используется для обнаружения будущей возможности JavaScript — metadata reflection API. Мы более подробно рассмотрим ее ближе к концу этой серии статей.

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

__decorate(
    [log],                                              // decorators
    C.prototype,                                        // target
   "foo",                                              // key
    Object.getOwnPropertyDescriptor(C.prototype, "foo") // desc
);

Сейчас мы внутри функции __decorate и, поскольку metadata reflection API недоступно, будет выполнен вариант, сгенерированный компилятором

// arguments.length === число аргументов, переданных в __decorate()
switch (arguments.length) {
   case 2: 
       return decorators.reduceRight(function(o, d) { 
           return (d && d(o)) || o; 
       }, target);
   case 3: 
       return decorators.reduceRight(function(o, d) { 
           return (d && d(target, key)), void 0; 
       }, void 0);
   case 4: 
       return decorators.reduceRight(function(o, d) { 
           return (d && d(target, key, o)) || o; 
       }, desc);
 }

Поскольку в нашем случае 4 аргумента было передано в метод __decorate, будет выбран последний вариант. Разобраться с этим кодом не так просто из-за бессмысленных названий переменных, но мы ведь не боимся, не так ли?

Давайте для начала узнаем, что делает метод reduceRight

reduceRight применяет аккумулирующую функцию к каждому элементу массива (в порядке справа-налево) и возвращает единственное значение.



Код ниже выполняет ровно ту же операцию, но переписан для простоты понимания:

 [log].reduceRight(function(log, desc) { 
    if(log) {
        return log(C.prototype, "foo", desc);
    } else {
        return desc;
    }
 }, Object.getOwnPropertyDescriptor(C.prototype, "foo"));

Когда этот код выполняется, то вызывается декоратор log и мы видим передаваемые в него параметры: C.prototype, "foo" и previousValue. То есть теперь мы знаем ответы на наши вопросы:

  • Откуда берутся эти аргументы?
  • Где именно вызывается функция log?

    Если мы вернемся к реализации декоратора log, мы сможем гораздо лучше понять, что же происходит при его вызове

    function log(target: Function, key: string, value: any) {

     // target === C.prototype
     // key === "foo"
     // value === Object.getOwnPropertyDescriptor(C.prototype, "foo")
    
     return {
         value: function (...args: any[]) {
    
             // конвертируем список аргументов, переданных в метод foo, в строку
             var a = args.map(a => JSON.stringify(a)).join();
    
             // вызываем foo() и получаем его результат
             var result = value.value.apply(this, args);
    
             // переводим результат в строку
             var r = JSON.stringify(result);
    
            // Отображаем информацию о вызове метода в консоли
            console.log(`Call: ${key}(${a}) => ${r}`);
    
            // возвращаем результат выполнения foo
            return result;
         }
     };

    }



После декорирования метод foo продолжит работать как обычно, но его вызовы будут также запускать дополнительную функциональность логирования, добавленную в декораторе log

var c = new C();
var r = c.foo(23); //  "Call: foo(23) => 46"
console.log(r);    // 46


Выводы



Неплохое приключение, да? Надеюсь, вам оно понравилось также, как мне. Мы только начали, а уже узнали, как делать некоторые действительно крутые вещи.

Декораторы методов могут быть испольованы для различных интересных "фишек". Например, если вы работали со "шпионами" (spy) в тестовых фреймворках вроде SinonJS, вас, возможно, восхитит возможность использования декораторов для того, чтобы создавать "шпионы" просто добавляя декоратор @spy.

В следующей части мы научимся работать с декораторами свойств.
Автор: @koroandr Remo H. Jansen

Комментарии (5)

  • –2
    Странно, что нету декораторов функций.
    • +2
      В декораторах есть смысл, когда они применяются к декларативно объявленным элементам. Обычную функцию можно «декорировать», просто объявив новую функцию и вызывая старую внутри нее. И никакого нового синтаксиса для этого не понадобится.
      • 0
        Как вы предлагаете переписать следующий код?
        @input_contract( n => n >= 0 )
        @log
        @memoize
        function fact( n ) {
            return n ? n * fact( n - 1 ) : n
        }
        
        • 0
          Объявление функции в вашем примере — это выражение (expression), а не утверждение (statement). Возникает вопрос — а с анонимными функциями или другими выражениями типа «функция» как поступать? Придется либо усложнить грамматику и добавить два разных типа объявления функции, либо разрешить писать что-то вроде:

          var myFunc = @a @b c;
          

          Имхо, оба варианта слишком спорные, чтобы их реализовывать на практике.

          Так что задачу правильнее всего решить по-старинке, реализовав некий helper, принимающий массив функций и компонующий их:

          let fact = decorate(
              () => input_contract(n => n >= 0),
              log,
              memoize,
              function(n) {
                  n ? n * fact( n - 1 ) : n
              }
          );
          
          • 0
            Нет, у меня это именно statement. А вот если бы были декораторы для expressions — было бы вообще шоколадно в плане создания DSL.

Только зарегистрированные пользователи могут оставлять комментарии. Войдите, пожалуйста.

Самое читаемое Разработка