TypeScript: новые возможности

Анатомия «декораторов» TypeScript и шаблоны их использования

В этом уроке мы узнаем о шаблоне декоратора в TypeScript и о том, как декораторы используются для изменения поведения классов. Мы также увидим, как пакет reflect-metadata помогает нам легко разрабатывать декораторы.

Декораторы - это аннотации, которые вы помещаете поверх объявления класса или члена класса, и они изменяют поведение этого класса или поля. Если вы являетесь разработчиком Angular, то вы, возможно, знаете о декораторе @Component, который определяет компонент Angular.

В приведенном выше примере аннотация @Component - это декоратор, украшающий класс AppComponent. По сути, он превращает этот класс в компонент Angular с конфигурацией, предоставленной с аннотацией декоратора. Аналогичным образом, аннотация @Input в поле экземпляра класса является декоратором.

Мы говорили о метапрограммировании и вариантах его использования в JavaScript в предыдущих уроках. В двух словах, метапрограммирование - это шаблон программирования для самоанализа и управления поведением программ. Например, декоратор @Component выше изменяет поведение класса AppComponent.

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

Декораторы не являются функцией TypeScript, в отличие от перечислений или интерфейсов. Это встроенная функция JavaScript, но ее еще предстоит стандартизировать. Это предложение является стадией-2 трека предложений ECMAScript. Однако мы можем реализовать декораторы в JavaScript с помощью подключаемого модуля Babel. Мы очень подробно исследовали декораторы в уроке Краткое руководство по декораторам JavaScript, где я также показал вам, как транспилировать декораторы с помощью Babel CLI.

Поскольку декораторы не являются частью предложения ECMAScript в настоящий момент и находятся на втором этапе трека предложений ECMAScript, он находится в активной разработке. В последнее время он претерпел значительные изменения, поэтому мне потребовалось пересмотреть приведенную выше статью о декораторах (старая версия здесь).

TypeScript реализовал декораторы, когда предложение было на ранней стадии, что означает, что предложение указывает (в настоящий момент), а то, что реализует TypeScript, больше не соответствует. Несмотря на то, что TypeScript реализует то, что мы теперь называем устаревшей версией предложения декораторов, декораторы довольно интересны и очень полезны.

💡 TypeScript не очень-то хочет вносить изменения в предложение декоратора. Поскольку эти изменения внесут критические изменения в приложение, и бог знает, сколько сторонних пакетов будет затронуто, я полагаю, что в интересах всех подождать, пока предложение не станет стабильным или не будет готово для включения в стандарт ECMAScript.

Поскольку декораторы не являются частью стандарта ECMAScript и в настоящее время считаются экспериментальными, TypeScript не позволит вам использовать декораторы без явного принятия на себя ответственности. Следовательно, вам необходимо установить experimentalDecorator compiler-option равным true в tsconfig.json файле или указать --experimentalDecorator флаг компилятора в команде.

{
    "files": [
        "program.ts"
    ],
    "compilerOptions": {
        "target": "ES6",
        "experimentalDecorators": true,
        "removeComments": true,
        "alwaysStrict": true
    }
}

💡 В приведенном выше файле tsconfig.json я установил target на ES6, чтобы было легче увидеть, как декораторы работают в скомпилированной программе, но вы также можете выбрать более низкие цели. TypeScript будет компилировать вниз декораторы и классы в соответствии с target выбранным вами.

Декораторы могут украшать только классы или их члены (такие как свойства, методы, средства доступа и т. Д.). TypeScript поддерживает декораторы для объявлений классов, методов, средств доступа (методов получения / установки), параметров метода (включая конструктор) и свойства класса.

Декоратор объявления класса

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

В приведенной выше программе мы объявили класс Person, который представляет собой простой класс JavaScript (ES6) с некоторыми статическими свойствами, некоторыми свойствами экземпляра и некоторыми методами экземпляра, поэтому здесь нет ничего особенного. Однако мы украсили этот класс декоратором @lock.

Согласно декоратору, lock - это функция JavaScript, которая будет вызываться механизмом JavaScript во время выполнения. Когда функция lock выполняется, она вызывается с функцией-конструктором класса в качестве единственного аргумента, поскольку этот декоратор украшает класс.

Этот аргумент сбивает с толку некоторых людей, поскольку мы декорируем класс, а полученный аргумент - это функция-конструктор. Что с этим? Ну, class - это модное ключевое слово, которое мы получили в ES6 для создания классов. Но под капотом (в движке JavaScript) он разбит на функцию конструктора и прототип. Вышеупомянутая программа аналогична приведенной ниже программе.

function Person(fname, lname) {
    this.fname = fname;
    this.lname = lname;
}
Person.version = 'v1.0.0';
Person.prototype.getFullName = function() {
    return this.fname + ' ' + this.lname;
}

Итак, функция-конструктор, о которой мы говорим, - это функция Person в приведенном выше примере. Это то, что мы получаем в функции декоратора в качестве единственного аргумента. Он также имеет prototype как статическое свойство с ним. Поэтому всякий раз, когда кто-то говорит class, представьте функцию-конструктор с телом class.constructor метода и его прототипом, имеющим все методы экземпляра этого класса.

В декораторе lock, используя метод Object.freeze(), мы заморозили и функцию-конструктор, и ее прототип, поэтому мы не можем добавлять или изменять какие-либо свойства для них во время выполнения, что очевидно из ошибок, показанных в комментариях.

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

Вышеупомянутая программа представляет собой скомпилированный код файла class-decorator.ts, который был скомпилирован с помощью команды tsc. Здесь __decorate - это вспомогательная функция, которая вызывает функцию декоратора lock с классом Person.

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

function decorator(class) {
  // modify 'class' or return a new one
}

В приведенном выше примере функция декоратора withInfo является универсальной. Параметр типа T представляет тип статической стороны класса, который эквивалентен аннотации :typeof Person. TypeScript неявно предоставляет этот тип класса функции декоратора, которую он украшает. Общие ограничения T extends Ctor проверяют только то, является ли входящий тип классом, поэтому этот декоратор может использоваться только для классов, а не для его внутренних членов.

💡 Мы обсудили Общие типы и Общие ограничения в этом уроке. Если вы хотите узнать больше о статическом типе класса, следуйте этому уроку.

Изнутри функции декоратора withInfo мы возвращаем новый класс, расширяющий класс Person. В этом новом классе нет constructor, что означает, что конструктор Person будет вызываться неявно. Объект person и его цепочка прототипов выглядят так, как показано ниже.

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

Декоратор метода украшает статический метод или метод экземпляра класса (кроме constructor). В этом случае функция-декоратор получает три аргумента. Первый аргумент - это target, которому принадлежит метод. Это может быть функция-конструктор (класс), если метод статический, или prototype класса, если метод является методом экземпляра.

function decorator(target, name, descriptor) {
  // modify 'descriptor' or return new one
}

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

В приведенном выше примере функция декоратора readonly изменяет только параметр writable дескриптора свойства, но функция декоратора prefix возвращает новый дескриптор свойства. Поле value дескриптора свойства метода содержит реализацию метода (function).

💡 Если для цели компиляции установлено значение ES3, функция декоратора метода не получит третий аргумент, который является дескриптором свойства. Также игнорируется возвращаемое значение (дескриптор свойства). Это связано с плохой поддержкой дескрипторов свойств в ES3.

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

Практически нет разницы между декоратором доступа и декоратором метода. Когда статический метод или метод экземпляра имеет префикс get или set, мы называем их аксессорами. Когда метод имеет префикс get (метод получения), его дескриптор свойства имеет поле get, которое содержит определение метода, а не поле value. Точно так же метод установки хранится в поле set.

class Person {
    constructor(
        public fname: string, public lname: string
    ) {}
    get fullname(): string {
        return this.fname + ' ' + this.lname;
    }
    set fullname( name ) {
        [ this.fname, this.lname ] = name.split(' ');
    }
}

Методы получения и установки с одинаковыми именами не имеют независимых дескрипторов свойств. Например, метод fullname в приведенном выше примере технически является единственным свойством, дескриптор свойства которого имеет поля get и set, содержащие эти реализации функций.

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

В приведенном выше примере, используя функцию декоратора uppercase, мы вернули новый дескриптор свойства для методов доступа fullname. Этот дескриптор свойства содержит поля get и set.

Декораторы недвижимости

Мы также можем украсить статические свойства класса и свойства экземпляра. Функция декоратора для этих свойств получает только два аргумента. Первый аргумент - это target, который может быть функцией-конструктором, если свойство равно static, или прототипом класса, если свойство является свойством экземпляра. Второй аргумент - это название свойства.

function decorator(target, name) {
  // collect or store some information
}

Этот декоратор получился немного необычным. Свойство экземпляра (поле AKA) создается в экземпляре при создании этого экземпляра. Следовательно, мы не можем действительно настроить дескриптор свойства экземпляра свойства в классе. В TypeScript нет механизма для украшения поля класса, но в предложении декоратора есть решение, которое мы видели в уроке Декораторы JavaScript.

Поэтому функция декоратора свойств не вызывается с дескриптором свойства в TypeScript. Также игнорируется возвращаемое значение функции декоратора. Так зачем нам дескриптор свойства? Что ж, его можно использовать для сбора информации о собственности, а затем использовать ее.



Введение в пакет« Reflection-metadata и его предложение ECMAScript
В этом уроке мы собираемся взглянуть на пакет Reflection-metadata , используемый TypeScript для разработки декораторов. … medium.com »



В приведенном выше уроке я говорил о предложении Отражать метаданные и о том, где его можно использовать. Пакет reflect-metadata является полифилом для этого предложения. Я бы порекомендовал вам прочитать этот урок, прежде чем двигаться дальше, иначе вы ничего не сможете понять. Так что сначала прочтите это.

Метод Reflect.defineMetadata(key, value, target, prop) определяет значение метаданных value с уникальным ключом key для объекта target или для свойства prop объекта target. Используя метод Reflect.getMetadata, вы можете извлечь эти метаданные позже.

В приведенном выше примере функция декоратора textCase устанавливает значение метаданных, используя метод Reflect.defineMetadata для target декоратора и свойство name с ключом case. Это значение метаданных представляет собой простой string, который сообщает о случае форматирования для этого свойства. Таким образом, fname следует преобразовать в uppercase, а lname в lowercase.

Позже мы извлекаем эти метаданные в fullname методе получения, используя метод Reflect.getMetdata. Поскольку this внутри метода экземпляра (или аксессора) указывает на экземпляр, this не то же самое, что target, который мы использовали в функции декоратора (который был Person.prototype) для хранить метаданные.

Но поскольку метод Reflect.getMetadata(key, target, prop) также ищет прототип аргумента target, который был бы прототипом this, который является Person.prototype, он смог найти значение метаданных. Однако с Reflect.getOwnMetadata() методом этого не произошло.

@Reflect.metadata(metadataKey, metadataValue)

Reflect.metadata() возвращает функцию декоратора. Поэтому вам не нужно самостоятельно писать функцию декоратора, как функцию декоратора textCase, которую мы написали выше.

Функция декоратора, возвращаемая этим методом, работает для всего, поэтому вы можете использовать ее для украшения классов, методов, средств доступа и т. Д. Этот декоратор используется для хранения некоторых метаданных для заданного target или property из target. Внутренне возвращенная функция декоратора вызывает метод Reflect.defineMetada с metadataKey, metadataValue и target и / или property.

Метод Reflect.metadata работает так же, как функция декоратора textCase. Reflect.metadata принимает ключ метаданных, а значение метаданных возвращает функцию декоратора, которая применяет метаданные к target (when class) или к property (other), который он украшает.

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

Декоратор параметров украшает параметры метода, включая параметры constructor. Функция декоратора для этих свойств получает три аргумента. Первый - это target, который может быть функцией-конструктором, если метод static, или прототипом класса, если метод является методом экземпляра.

Второй аргумент - это имя метода, а третий аргумент - порядковый индекс параметра в определении метода.

function decorator(target, name, index) {
  // collect or store some information
}

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

В приведенном выше примере функция декоратора textCase украшает параметры constructor. Имя метода name для функции-конструктора в функции-декораторе используется как undefined, а target - как сам класс, поскольку он принадлежит классу, а не прототипу.

Используя аргумент index, мы сохраняем значение метаданных для каждого параметра. Поскольку значение метаданных идентифицируется с индексом параметра, мы использовали значения 0 и 1 вместе с undefined в качестве имени метода для извлечения этих метаданных в методе получения fullname.

Улучшение декораторов

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

Фабрика декораторов - это функция, возвращающая декоратор. Таким образом, технически метод Reflect.metadata, который мы использовали ранее, является фабрикой декораторов, поскольку он возвращает фактическую функцию декоратора. Если вы хотите украсить что-то с помощью фабрики декораторов, нам нужно вызвать эту функцию в синтаксисе аннотации декоратора, таком как @decoFactory(...args).

Как вы можете видеть в приведенном выше примере, функция version - это фабрика декораторов. Следовательно, он должен быть аннотирован сигнатурой вызова @version(...) при украшении класса.

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

Цепочка декораторов

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

@decoratorA
@decoratorB
entity

Вы также можете положить их бок о бок, как показано ниже. В этом случае декораторы оцениваются слева направо, но применяются справа налево.

@decoratorA @decoratorB entity

В обоих случаях сначала оценивается decoratorA, так что если decoratorA является фабрикой декораторов, функция декоратора собирается. После оценки всех декораторов они применяются в обратном порядке. Следовательно, сначала применяется decoratorB, а затем decoratorA. Давайте посмотрим на это в действии.

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

Заказ украшения

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

В приведенном выше примере фабрика декораторов factory такая же, как decoratorFactory, использованная в предыдущем примере. В этом примере мы украсили почти все, что можем украсить. По результатам мы видим порядок декорирования.

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

Создать метаданные декоратора

TypeScript предоставляет параметр компилятора emitDecoratorMetadata (или флаг компилятора --emitDecoratorMetadata) для неявного добавления определенных метаданных к классу или его членам. Если для этого параметра установлено значение true, компилятор TypeScript вставляет некоторый дополнительный код в скомпилированный JavaScript при использовании декораторов.

{
    "files": [
        "program.ts"
    ],
    "compilerOptions": {
        "target": "ES6",
        "experimentalDecorators": true,
        "emitDecoratorMetadata": true,
        "removeComments": true,
        "alwaysStrict": true
    }
}

В приведенной выше программе мы создали простую функцию-декоратор noop, которая ничего не делает. Затем мы украсили этим декоратором класс Person и метод getNameWithPrefix. Внутри метода getNameWithPrefix мы извлекли некоторые метаданные с помощью метода Reflect.getMetadata и некоторых странно выглядящих ключей. Когда и где мы установили эти метаданные?

Чтобы понять, где были установлены эти метаданные, нам нужно увидеть скомпилированный код JavaScript, потому что логика для добавления этих метаданных добавляется во время компиляции компилятором TypeScript.

Как видно из приведенного выше фрагмента кода, компилятор TypeScript автоматически применил еще несколько декораторов (вместе с noop) к методу getNameWithPrefix, а также к классу Person с помощью функции справки __metadata, которая представляет собой не что иное, как фабрика декораторов.

Эта фабрика декораторов возвращает декоратор, который хранит метаданные для данной цели или ее свойства с помощью метода Reflect.metdata. Эти декораторы будут применяться только к классу или членам класса с аннотациями декораторов. Роль этих декораторов заключается в следующем.

  • Этот декоратор хранит тип данных времени выполнения объекта, который он украшает с помощью ключа design:type.
  • Этот декоратор хранит тип данных среды выполнения аргументов метода, который он украшает ключом design:paramtypes.
  • Этот декоратор хранит тип данных среды выполнения для возвращаемого значения метода, который он украшает с помощью ключа design:returntype.

Здесь тип данных среды выполнения - это значение метаданных. Этот тип данных сериализуется из типа TypeScript. Например, string сериализуется в String, а number сериализуется в Number. Вы можете следовать этому списку, чтобы узнать больше о сериализации других типов данных.

На что нужно обратить внимание

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

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

Кроме того, вы могли заметить, что мы использовали утверждения типа Person as any в некоторых программах. Например, если вы возьмете пример фабрики декораторов классов, декоратор version добавляет статическое свойство version в класс Person. Но если вы получите доступ к свойству version на Person, компилятор TypeScript пожалуется.

Property 'version' does not exist on type 'typeof Person'.
console.log( 'version ->', Person.version );
                                  ~~~~~~~

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