Темна сторона TypeScript і ES7 — @декоратори на прикладах

Декоратори — це неймовірно круто. Вони дозволяють описувати мета інформацію прямо в оголошенні класу, групуючи все в одному місці і уникаючи дублювання. Жахливо зручно. Одного разу спробувавши, ви вже ніколи не погодитеся писати по-старому.
Проте, незважаючи на всю корисність, декоратори в TypeScript (вони ж декоратори в прийдешньому ES7 не так прості, як хотілося б. Робота з ними вимагає навичок джедая, так як необхідно розбиратися в об'єктної моделі JavaScript (ну, ви зрозуміли, про що я), API кілька заплутаний і, до того ж, ще не стабільний. У цій статті я розповім трохи про будову декораторів і покажу кілька конкретних прийомів, як поставити цю темну силу на благо front-end розробки.
Крім TypeScript, декоратори доступні в Babel. У цій статті розглядається тільки реалізація в TypeScript.


  Основы
Декорувати в TypeScript можна класи, методи, параметри методу, методи доступу властивості (accessors) і поля.
Чому я використовую термін 'поле', а не 'властивість' як в офіційній документаціїВ TypeScript термін "поле", зазвичай, не використовується, і поля називають також властивостями (property). Це створює велику плутанину, оскільки різниця є. Якщо ми оголошуємо властивість з методами доступу get/set, то в оголошенні класу появляется виклик Object.defineProperty і в декораторе доступний дескриптор, а якщо оголошуємо просто поле (в термінах C# і Java) не з'являється нічого, і, відповідно, дескриптор не передається в декоратор. Це визначає сигнатуру декораторів, тому я використовую термін "поле", щоб відрізняти їх від властивостей з методами доступу.
У загальному випадку, декоратор — це вираз, яка символом "@", яке повертає функцію певного виду (різного в кожному випадку). Власне, можна просто оголосити таку функцію і використовувати ім'я в якості вираження декоратора:
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, де можна подивитися, у що точно компілюються декоратори і спробувати їх у дії.
Вид декоратора Сигнатура функції
Декоратор класу
Приклад playground

@MyDecorator
class MyClass {}

function MyDecorator<TFunction extends Function>(target: TFunction): TFunction {
  return target;
}
  • target — конструктор класу
  • returns — конструктор класу або null. Якщо повернути конструктор, то він замінить оригінальний. При цьому необхідно також налаштувати прототип в новому режимі конструктора.
Декоратор методу
Приклад playground

class MyClass {
@MyDecorator
myMethod(){}
}

function MyDecorator(target: Object, propertyKey: string | symbol, descriptor: TypedPropertyDescriptor<any>): TypedPropertyDescriptor<any> {
  return descriptor;
}
  • target — прототип класу
  • propertyKey — ім'я методу (зберігається при минификации); в поточній реалізації тип — string
  • descriptorдескриптор методи*
  • returns — дескриптор методу* або null
Декоратор статичного методу
Приклад playground

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

function MyDecorator(target: Function, propertyKey: string | symbol, descriptor: TypedPropertyDescriptor<any>): TypedPropertyDescriptor<any> {
  return descriptor;
}
  • target — конструктор класу
  • propertyKey — ім'я методу (зберігається при минификации); в поточній реалізації тип — string
  • descriptorдескриптор методи*
  • returns — дескриптор методу* або null
Декоратор методів доступу
Приклад playground

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

Аналогічно методу. Декоратор слід застосовувати до першого методу доступу (get або set), у порядку оголошення в класі.
Декоратор
Приклад playground

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

function MyDecorator(target: Object, propertyKey: string | symbol, index: number): void { }
  • target — прототип класу
  • propertyKey — ім'я методу (зберігається при минификации); в поточній реалізації тип — string
  • index — індекс параметра у списку параметрів
  • returns — void

Декоратор поля (властивості)
Приклад playground

class MyClass {
@MyDecorator
myField: number;
}

function MyDecorator(target: Object, propertyKey: string | symbol): TypedPropertyDescriptor<any> {
return null;
}
  • target — прототип класу
  • propertyKey — ім'я поля (зберігається при минификации); в поточній реалізації тип — string
  • returns — null або дескриптор властивості; якщо повернути ручку, то він буде використаний для виклику Object.defineProperty; однак, при підключенні бібліотеки reflect-metadata цього не відбувається (це баг в reflect-metadata
Декоратор статичного поля (властивості)
Приклад playground

class MyClass {
@MyDecorator
static myField;
}

function MyDecorator(target: Function, propertyKey: string | symbol): TypedPropertyDescriptor<any> {
return null;
}
  • target — конструктор класу
  • propertyKey — ім'я поля (зберігається при минификации); в поточній реалізації тип — string
  • returns — null або дескриптор властивості; якщо повернути ручку, то він буде використаний для виклику Object.defineProperty; однак при підключенні бібліотеки reflect-metadata цього не відбувається (це баг в reflect-metadata
Інтерфейси Декоратори інтерфейсів та їх членів не підтримуються.
Оголошення типів Декоратори в оголошеннях типів (ambient declarations) не підтримуються.
Функції та змінні поза класу
Декоратори поза класу не підтримуються.
Інтерфейс TypedPropertyDescriptor<T>, що фігурує в сигнатурі декораторів методів і властивостей оголошений наступним чином:
interface TypedPropertyDescriptor<T> {
перечіслімого?: boolean;
configurable?: boolean;
writable?: boolean;
value?: T;
get?: () => T;
set?: (value: T) => void;
}

Якщо вказати в оголошенні декоратора конкретний тип T для TypedPropertyDescriptor, то можна обмежити тип властивостей, до яких декоратор застосуємо. Що означають члени цього інтерфейсу — можна подивитися здесь. Якщо коротко, для методу value містить власне сам метод, для поля — значення, для властивості — get і set містять відповідні методи доступу.
Налаштування середовища
Підтримка декораторів експериментальна і може змінитися в майбутніх релізах (TypeScript 2.0 не змінилася). Тому необхідно додати experimentalDecorators: true tsconfig.json. Крім того, декоратори доступні тільки якщо target: es5 або вище.
tsconfig.json
{
"compilerOptions": {
"target": "ES5",
"experimentalDecorators": true
}
}


  Важно!!!про target: ES3 і JSFiddle

Важливо не забути вказати опцію target — ES5 при роботі з декораторами. Якщо цього не зробити, то код відбудеться створення без помилок, але працювати буде по-іншому (це баг в компіляторі TypeScript). Зокрема, декораторам методів і властивостей не буде передаватися третій параметр, а їх значення, що повертається, буде ігноруватися.

Ці явища можна спостерігати в JSFiddle (це вже баг в JSFiddle), тому в даній статті я не розміщую приклади в JSFiddle.

Тим не менш, є обхідні рішення для цих багів. Потрібно просто самим отримувати дескриптор, і самим же його оновлювати. Наприклад, ось реалізація декоратора @safe, яка працює як з target ES3, так і з ES5.
Для використання інформації про типи необхідно також додати emitDecoratorMetadata: true.
tsconfig.json
{
"compilerOptions": {
"target": "ES5",
"experimentalDecorators": true,
"emitDecoratorMetadata": true
}
}

Для використання класу Reflect необхідно встановити додатковий пакет reflect-metadata:
npm install reflect-metadata --save

І в коді:
import "reflect-metadata";

Однак якщо ви використовуєте Angular 2, то ваша система складання вже може містити в собі реалізацію Reflect, і після інсталяції пакета reflect-metadata ви можете отримати runtime помилку
Unexpected value 'YourComponent' exported by the module 'YourModule'
. В цьому випадку краще встановити тільки typings.
typings install dt~reflect-metadata --global --save

Отже, перейдемо до практики. Розглянемо кілька прикладів, які демонструють можливості декораторів.
@safeавтоматична обробка помилок всередині функції

Припустимо, у нас часто зустрічаються другорядні функції, помилки всередині яких ми хотіли б ігнорувати. Писати кожен раз 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");

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

Спробувати в дії Plunker
Подивитися в Playground
@OnChangeзавдання обробника зміни значення поля

Припустимо, при зміні значення поля потрібно виконати якусь логіку. Можна, звичайно, визначити властивість 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, перечіслімого: 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, з-за якого для декоратора полів повертається значення ігнорується.
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;

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

Спробувати в дії Plunker
Подивитися в Playground
Нам довелося обробник оголосити як static, т. к. важко сосласться на экземплярный метод. Ось альтернативний варіант з рядковим параметром інший з використанням лямбды.
@Injectвпровадження залежностей

Однією з цікавих особливостей декораторів є можливість отримувати інформацію про тип декорируемого властивості або параметри (скажімо "дякую" 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, з-за якого для декоратора полів повертається значення ігнорується.
// Використовувати інтерфейс, на жаль, не вийде
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, а декоратор вже самостійно визначає його тип і визначає метод доступу, який отримує відповідний примірник сервісу. Красиво і зручно. Результат виконання:

Спробувати в Plunker
Подивитися в Playground
@JsonNameсеріалізація моделей c перетворенням


Припустимо, з якихось причин необхідно перейменувати деякі поля об'єкта при серіалізації в 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);

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

Спробувати в Plunker
Подивитися в Playground
Наведений декоратор володіє тим недоліком, що, якщо модель містить в якості полів об'єкти інших класів, то поля цих класів ніяк не обробляються методом 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);
}
}

Розберемо, як це реалізовано.
По-перше, нам необхідно визначити декоратор поля 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, звідки ми будемо брати метадані. Ми використовуємо декоратори полів, тому метадані потрібно брати з прототипу класу. Для декораторів статичних членів потрібно використовувати конструктор класу. Отримати прототип можна, викликавши Object.getPrototypeOf або ж звернувшись до властивості 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.
@Controller, Actionсервіси для взаємодії з сервером

У 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"});

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



Спробувати в Plunker
Подивитися в Playground

Можна також додати декоратори параметрів так, щоб сигнатура методу в TypeScript повністю повторювала сигнатуру серверного методу. З допомогою декораторів можна зберігати ім'я кожного параметра і при виконанні запиту формувати на основі цих даних відповідний JSON. На жаль, отримати ім'я параметра в коді декоратори не дозволяють, тому доведеться передавати ім'я в декоратор вручну (так само, як декоратор Controller).
Висновок
Декоратори в TypeScript є настільки потужним інструментом, що буквально дозволяють розширювати мову новою функціональністю. В цьому плані вони навіть чимось нагадують препроцесор в С++, з допомогою якого можна здорово облагородити або заплутати свій код.
Як було продемонстровано в статті, в нашому розпорядженні наступний ряд прийомів:
  1. Модифікація дескриптора методу або властивості. Зокрема, можна підмінити метод обгорткою, задати дескриптор для поля, з оголошенням методів доступу і т. д. В цілому, з декоратора можна зробити будь-яку трансформацію прототипу класу.
  2. Збереження і використання метаданих за допомогою класу Reflect. Ми можемо передати в декоратор будь-яке значення, також нам доступно ім'я властивості або методу, стійке до минифкации.
  3. Отримання інформації про тип за допомогою виклику Reflect.getMetada з ключем "design:type".
Використання цих прийомів може бути самим різноманітним, залежно від конкретних потреб. Наприклад, у Легкій Клієнті 8 ми активно використовуємо декоратори для оголошення сервісів взаємодії з сервером. Наша реалізація трохи складніше поданої в статті (ми використовуємо декоратори параметрів), але в цілому побудована за тим же принципом. Крім того, ми думаємо ще задіяти кілька декораторів для публічного оголошення API наших ReactJS компонентів, а також автоматизувати прив'язку обробників подій до this.
На цьому поки все. Пишіть враження у коментарях, діліться своїм досвідом використання декораторів.

Джерело: Хабрахабр

0 коментарів

Тільки зареєстровані та авторизовані користувачі можуть залишати коментарі.