082 Вводное видео

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

Декораторы - это функции, которые оборачивают нужный нам функционал и обогащают логику работы программы (или данного функционала)

083 Паттерн декоратора

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

Паттерн представляет собой использование функции, в которую вкладывается тип объекта (в примере - интерфейс) и ретёрнется этот же объект, но модифицированный.

По сути своей, работаем извне мы уже над модифицированным объектом

interface IUserService {  
    users: number;  
    getUsersInDatabase(): number;  
}  
  
class UserService implements IUserService {  
    public users: number = 1000;  
  
    getUsersInDatabase(): number {  
        return this.users;  
    }  
}  
  
function nullUser(obj: IUserService) {  
    obj.users = 0; // Какая-то логика... 
    return obj;  
}  
 
// Вывод обычного инстанса
console.log(new UserService().getUsersInDatabase());  
// Вывод задекорированного инстанса
console.log(nullUser(new UserService()).getUsersInDatabase());

А вот и объяснение, почему декораторы на классах работают снизу вверх: верхний декоратор покрывает выполнение нижнего

function logUser(obj: IUserService) {  
    console.log('Users: ' + obj.users);  
    return obj;  
}  
  
console.log(new UserService().getUsersInDatabase());  
 
console.log(nullUser(new UserService()).getUsersInDatabase());  
 
console.log(logUser(nullUser(new UserService())).getUsersInDatabase()); // 'Users: 0'

Декораторы, в своей роли, выступают некоторым синтаксическим сахаром, который позволяет более удобно реализовывать данный паттерн

084 Декоратор класса

Чтобы начать использовать декораторы, нужно включить их в ТС

  • Сама функция декоратора принимает в себя таргет, типом которого является функция.
  • Чтобы изменить значения объекта, нужно обратиться к нему через прототип (потому что таргет - это функция)
  • Декораторы используются до того, как создастся объект, поэтому если внутри класса будет присвоение числа, то оно сохранится и не изменится декоратором. Поэтому все изменения и присвоения значений должны происходить внутри декоратора
@nullUser // Объявление декоратора
class UserService implements IUserService {  
    public users: number = 1000;  
  
    getUsersInDatabase(): number {  
        return this.users;  
    }  
}  
  
function nullUser(target: Function) {  // Функция декоратора
    target.prototype.users = 0;  // Изменение значения
}
 
// Результат = 1000
console.log(new UserService().getUsersInDatabase());
@nullUser // Объявление декоратора
class UserService implements IUserService {  
    public users: number;  
  
    getUsersInDatabase(): number {  
        return this.users;  
    }  
}  
  
function nullUser(target: Function) {  // Функция декоратора
    target.prototype.users = 0;  // Изменение значения
}
 
// Результат = 0
console.log(new UserService().getUsersInDatabase());

Так же у нас есть второй вариант создания функции-декоратора - это объявление конструируемого класса (дженерик T) и возвращение анонимного класса, который расширяется от получаемого класса Этот вариант имеет больший приоритет над первым вариантом и оригинальным объектом. Тут мы непосредственно работаем с переменными декорируемого класса и эти изменения будут задаваться даже поверх тех значений, что находятся внутри класса (в отличие от первого способа, где приоритет оригинального класса выше изменения прототипов функции)

@numUserAdvanced
class UserService implements IUserService {  
    public users: number = 1000;  
  
    getUsersInDatabase(): number {  
        return this.users;  
    }  
}  
 
function numUserAdvanced<T extends { new(...args: any[]): {} }>(constructor: T) {  
    return class extends constructor {  
        users = 3;  
    }  
}
 
// Результат = 3
console.log(new UserService().getUsersInDatabase());

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

Фабрика декораторов - это функция, которая производит нужные нам декораторы.

@setUsers(2)  // !
class UserService implements IUserService {  
    public users: number;  
  
    getUsersInDatabase(): number {  
        return this.users;  
    }  
}  
 
function setUsers(users: number) {  // !
    // стрелочная функция, чтобы не терять контекст  
    return (target: Function) => { 
        target.prototype.users = 0;  
    }  
}
 
console.log(new UserService().getUsersInDatabase()); // = 2

Так же в фабрику мы можем превратить нашу функцию для конструирования класса

// Обычный декоратор
function numUserAdvanced<T extends { new(...args: any[]): {} }>(constructor: T) {  
    return class extends constructor {  
        users = 3;  
    }  
}  
 
// Фабрика декораторов
function setNumUsersAdvanced(users: number) {  
    return <T extends {new(...args: any[]):{}}> (constructor: T) => {  
        return class extends constructor {  
            users = users;  
        }        
    }  
}
@setNumUsersAdvanced(4)  // !
class UserService implements IUserService {  
    public users: number = 1000;  // !
  
    getUsersInDatabase(): number {  
        return this.users;  
    }  
}
 
function setNumUsersAdvanced(users: number) {  // !
    return <T extends {new(...args: any[]):{}}>(constructor: T) => {  
        return class extends constructor {  
            users = users;  
        }    
    }  
}  
 
// Результат 4 
console.log(new UserService().getUsersInDatabase());

И тут стоит упомянуть, в каком порядке выполняются декораторы. Порядок инициализации - прямой, а уже исполнение происходит в обратном порядке.

@setUsers(2)  
@log()  
class UserService implements IUserService {  
    public users: number = 1000;  
  
    getUsersInDatabase(): number {  
        return this.users;  
    }  
}
 
function setUsers(users: number) {  
    console.log('setUsers init');  
    return (target: Function) => {  
        console.log('setUsers run');  
        target.prototype.users = 0;  
    }  
}  
  
function log() {  
    console.log('log init');  
    return (target: Function) => {  
        console.log('log run');  
        console.log(target);  
    }  
}

086 Упражнение -  Декоратор CreatedAt

Нам нужно создать декоратор CreatedAt который будет добавлять в класс дату создания его инстанса

interface IUserService {  
    users: number;  
    getUsersInDatabase(): number;  
}  
  
@CreatedAt  
class UserService implements IUserService {  
    users: number;  
  
    getUsersInDatabase(): number {  
        return this.users;  
    }  
}  
  
function CreatedAt<T extends { new (...args: any[]): {} }>(constructor: T) {  
    return class extends constructor {  
        createdAt: Date = new Date();  
    }  
}  
 
// Выведет количество пользователей и дату
console.log((new UserService());

Однако мы можем встретиться с такой проблемой, что обратиться непосредственно к сгенерированному свойству или методу у нас не получится извне (так как ТС ориентируется ещё и на рантайм-код)

Поэтому, чтобы решить проблему с недоступными извне свойствами, нам нужно создать отдельный тайп, который будет явно указывать, что у нас есть нужное нам свойство или метод в инстансе класса

interface IUserService {  
    users: number;  
    getUsersInDatabase(): number;  
}  
  
type CreatedAt = {  // !
    createdAt: Date;  
}  
  
@CreatedAt  
class UserService implements IUserService {  
    users: number;  
  
    getUsersInDatabase(): number {  
        return this.users;  
    }  
}  
  
function CreatedAt<T extends { new (...args: any[]): {} }>(constructor: T) {  
    return class extends constructor {  
        createdAt: Date = new Date();  
    }  
}  
 
// !
console.log((new UserService() as IUserService & CreatedAt).createdAt); // OK

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

Структура декоратора метода выглядит таким образом:

  • Сначала принимает в себя метод
  • Потом наименование метода
  • Потом дескриптор метода (который хранит всю важную для нас информацию)
  • Далее возвращаем дескриптор или войд от декоратора
  • И уже дальше можем прописать дополнительную логику, которая должна выполняться до самого метода (выполнится декоратор потом метод)
class UserService implements IUserService {  
    public users: number = 1000;  
  
    @Log  
    getUsersInDatabase(): number {  
        return this.users;  
    }  
}  
  
function Log(  
	// Объект, к которому относится метод  
    target: Object,
    // Название метода  
    propertyKey: string | symbol, 
    // Дескриптор, который принимает в себя функцию   
    descriptor: TypedPropertyDescriptor<(...args: any[]) => any> 
    // Вот так выглядит корректная типизация декоратора метода:
): TypedPropertyDescriptor<(...args: any[]) => any> | void {  
    console.log(target);  
    console.log(propertyKey);  
    console.log(descriptor);  
}  

Что мы можем менять через дескриптор? Мы можем задать модификаторы для метода: его конфигурируемость, читабелность, поменять значения, задать get/set

Так же мы можем менять логику выполнения функции. Через обращение к значению дескриптора, мы можем задать новое значение для нашей функции и логика выполнения этой функции так же поменяется. На примере можно увидеть, что теперь вместо Error функция выдаёт 'no error'

class UserService implements IUserService {  
    public users: number = 1000;  
  
    @Log  
    getUsersInDatabase(): number {  
        throw new Error('Ошибка');  // ! Выкинет ошибку
    }  
}  
  
function Log(  
    target: Object, 
    propertyKey: string | symbol,
    descriptor: TypedPropertyDescriptor<(...args: any[]) => any>
): TypedPropertyDescriptor<(...args: any[]) => any> | void {  
    console.log(target);  
    console.log(propertyKey);  
    console.log(descriptor);  
  
    descriptor.value = () => {  
        console.log('no error');  // ! Заменим функцию на другую
    }  
}

Мы можем не только переопределять значения функции, но и дополнять их. Можно заранее сохранить значение дескриптора и вызвать его во время изменения, чтобы дополнить логику нашего метода (а не перетереть её)

function Log(  
    target: Object, 
    propertyKey: string | symbol,
    descriptor: TypedPropertyDescriptor<(...args: any[]) => any>
): TypedPropertyDescriptor<(...args: any[]) => any> | void {  
    console.log(target);  
    console.log(propertyKey);  
    console.log(descriptor);  
 
	const oldValue = descriptor.value; // !
 
    descriptor.value = () => {  
	    oldValue(); // !
        console.log('no error');  // ! Дополним функцию
    }  
}

Так же ничто нам не мешает сделать фэктори декоратор и для метода вот таким вот образом:

class UserService implements IUserService {  
    public users: number = 1000;  
  
    @Log()  
    getUsersInDatabase(): number {  
        throw new Error('Ошибка');  
    }  
}  
 
// Factoory Decorator
function Log() {  
    return (  
        target: Object,  
        propertyKey: string | symbol,  
        descriptor: TypedPropertyDescriptor<(...args: any[]) => any>  
    ): TypedPropertyDescriptor<(...args: any[]) => any> | void  => {  
        console.log(target);  
        console.log(propertyKey);  
        console.log(descriptor);  
    }   
}

088 Упражнение - Декоратор перехвата ошибок

Нам нужно создать декоратор, который будет обрабатывать ошибку и выводить её в зависимости от значения аргумента rethrow

interface IUserService {  
    users: number;  
    getUsersInDatabase(): number;  
}  
  
class UserService implements IUserService {  
    users: number;  
  
    @Catch({rethrow: true})  
    getUsersInDatabase(): number {  
        throw new Error('Ошибка'); // Выводим ошибку
    }  
}  
  
function Catch({ rethrow }: { rethrow: boolean } = {rethrow: false}) {  
    return (  
        target: Object,  
        propertyKey: string | symbol,  
        descriptor: TypedPropertyDescriptor<(...args: any[]) => any>  
    ): TypedPropertyDescriptor<(...args: any[]) => any> | void => {  
        // Сохраняем оригинальный метод
        const method = descriptor.value; 
        // Пишем логику для нового метода 
        descriptor.value = async (...args: any[]) => {  
            try { // Если ошибкинет, то  
	            // Вызываем срабатывание оригинального метода
                return await method?.apply(target, args);  
            } catch(e) {  // Если ошибка есть, то
                if (e instanceof Error) {  // Если е это Error, то
                    console.log(e.message);  // Выводим сообщение
                    // Если rethrow=true, то выводим ошибку
                    if (rethrow) { 
                        throw e;  // Выводим существующую ошибку
                    }  
                }  
            }  
        }  
    }  
}

И тут сразу хочется отметить. что если мы оставим async/await, то у нас в выводе будет фигурировать Promise

089 Декоратор свойства

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

Декоратор свойства отличается от декоратора метода только тем, что в него мы уже не передаём дескрипторы (их нужно будет редактировать через defineProperty).

Цель такая: нам нужно написать декоратор свойства, который задаст максимальное значение для свойства = 100

interface IUserService {  
    users: number;  
    getUsersInDatabase(): number;  
}  
  
class UserService implements IUserService {  
    @Max(100) // !  
    public users: number = 1000; // !  
  
    getUsersInDatabase(): number {  
        throw new Error('Ошибка');  
    }  
}  
  
function Max(max: number) {  
    return (  
        target: Object, // UserService  
        propertyKey: string | symbol // users  
    ) => {   
    }  
}  

Первым делом, нам нужно описать геттеры и сеттеры для свойства, чтобы была возможность регулировать задание нового значения и получение

function Max(max: number) {  
    return ( target: Object, propertyKey: string | symbol ) => {  
        let value: number;  
  
        const setter = function (newValue: number): void {  
            if (newValue > max) {  
                console.log(`Число не может быть больше ${max}`);  
            } else {  
                value = newValue;  
            }  
        }  
  
        const getter = function (): number {  
            return value;  
        } 
    }  
}  

Так же мы можем через defineProperty поменять дескрипторы для свойств и заменить стоковые методы (get/set) и модификаторы (читабельность, изменяемость и так далее) для этих свойств

function Max(max: number) {  
    return ( target: Object, propertyKey: string | symbol ) => {  
        let value: number;  
          
        const setter = function (newValue: number): void {  
            if (newValue > max) {  
                console.log(`Число не может быть больше ${max}`);  
            } else {  
                value = newValue;  
            }  
        }  
  
        const getter = function (): number {  
            return value;  
        }  
 
		// ! Переопределение геттеров и сеттеров для свойства
        Object.defineProperty(target, propertyKey, {  
            get: getter,  
            set: setter  
        });  
    }  
}  

Теперь мы спокойно можем пользоваться нужным нам функционалом и значение больше 100 задать не сможем - получим предупреждение

const userService = new UserService(); // Error потому что изначально стоит 1000  
  
userService.users = 1;  
console.log(userService.users); // 1  
  
userService.users = 101;  
console.log(userService.users); // error

090 Декоратор accessor

Декоратор accessor используется не так часто, как другие декораторы. Конкретно он покрывает выполнение геттеров и сеттеров. Нам достаточно написать данный метод один раз над одним аццессором и он будет покрывать сразу оба (на оба навешивать не надо - это будет ошибкой)

interface IUserService {  
    getUsersInDatabase(): number;  
}  
  
class UserService implements IUserService {  
    private _users: number = 1000;  
  
    @Log()  
    set users(users: number) {  
        this._users = users;  
    }  
  
    get users() {  
        return this._users;  
    }   
}  
  
function Log() {  
    return (  
        target: Object, // UserService  
        _: string | symbol, // users  
        descriptor: PropertyDescriptor //  
    ) => {     
    }  
}  

Дескриптор хранит ту же самую информацию, что и дескриптор для методов

И конкретно тут мы описали логику модификации сеттера (геттер работает по прежнему) set?.apply(target, args) - применяем сеттер на таргете (нашем классе) и вкладываем в качестве аргумента принимаемые значения

interface IUserService {  
    getUsersInDatabase(): number;  
}  
  
class UserService implements IUserService {  
    private _users: number = 1000;  
  
    @Log()  
    set users(users: number) {  
        this._users = users;  
    }  
  
    get users() {  
        return this._users;  
    }  
  
    getUsersInDatabase(): number {  
        throw new Error('Ошибка');  
    }  
}  
  
function Log() {  
    return (  
        target: Object, // UserService  
        _: string | symbol, // users  
        descriptor: PropertyDescriptor //  
    ) => {  
        // Сохраняем старый сеттер  
        const set = descriptor.set;  
        // Переопределим сеттер, добавив ему логику  
        descriptor.set = (...args: any) => {  
            console.log(args); // [ 1 ]  
            set?.apply(target, args);  // !
        }    
    }  
}  
  
const userService = new UserService();  
userService.users = 10;  
console.log(userService.users); // 1

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

Декораторы параметров зачастую используются для метапрограммирования

interface IUserService {  
    getUsersInDatabase(): number;  
}  
  
class UserService implements IUserService {  
    private _users: number;  
  
    getUsersInDatabase(): number {  
        return this._users;  
    }  
  
    setUsersInDatabase(
	    @Positive() users: number,
	    @Positive() __: number
    ): void {  
        this._users = users;  
    }  
}  
  
function Positive() {  
    return (  
        target: Object, // UserService - класс  
        propertyKey: string | symbol, // setUsersInDatabase - метод  
        parameterIndex: number, // индекс среди аргументов  
    ) => {  
        console.log(target);  
        console.log(propertyKey);  
        console.log(parameterIndex);  
    }  
}  
  
const userService = new UserService();

092 Метаданные

Для начала нам нужно активировать метаданные в ТС, и уже затем после компиляции мы будем получать нативный JS с метаданными, которые б

И далее нужно установить саму библиотеку для работы с метаданными

npm init
npm i reflect-metadata

И через Reflection мы получаем возможность использовать методы для обработки метаданных. Метод getOwnMetadata() принимает в себя три аргумента: тип запрашиваемых метаданных, класс и наименование метода внутри этого класса. Типы метаданных мы можем найти в документации. Используется для валидации типов или для связывания компонентов (dependency injection)

import 'reflect-metadata';  // !
  
interface IUserService {  
    getUsersInDatabase(): number;  
}  
  
class UserService implements IUserService {  
    private _users: number;  
  
    getUsersInDatabase(): number {  
        return this._users;  
    }  
  
    setUsersInDatabase(@Positive() users: number): void {  
        this._users = users;  
    }  
}  
 
// MetaProgramming
function Positive() {  
    return (  
        target: Object, // UserService - класс  
        propertyKey: string | symbol, // setUsersInDatabase - метод  
        parameterIndex: number, // 0 - первый среди аргументов  
    ) => {  
    
    // !
        console.log(Reflect.getOwnMetadata('design:type', target, propertyKey));  
        console.log(Reflect.getOwnMetadata('design:paramtypes', target, propertyKey));  
        console.log(Reflect.getOwnMetadata('design:returntype', target, propertyKey));  
    }  
}  
//
 
const userService = new UserService();  
console.log(userService);

Метод Positive навешивает в метаданные каждого аргумента метку, что значение позитивно и оно нам подходит

function Positive() {  
    return (  
        target: Object, // UserService - класс  
        propertyKey: string | symbol, // setUsersInDatabase - метод  
        parameterIndex: number, // 0 - первый среди аргументов  
    ) => {  
        // Добавляем символ позитивности в метаданных для аргументов  
        let existParams: number[] = Reflect.getOwnMetadata(POSITIVE_METADATA_KEY, target, propertyKey) || [];  
        existParams.push(parameterIndex);  
        Reflect.defineMetadata(POSITIVE_METADATA_KEY, existParams, target, propertyKey)  
    }  
}

И тут уже показана реализация валидации аргументов метода. Сначала мы присваиваем марку валидируемости для одного значения, а уже через Validate делаем общую проверку валидных значений

import 'reflect-metadata';  
  
const POSITIVE_METADATA_KEY = Symbol('POSITIVE_METADATA_KEY');  
  
interface IUserService {  
    getUsersInDatabase(): number;  
}  
  
class UserService implements IUserService {  
    private _users: number;  
  
    getUsersInDatabase(): number {  
        return this._users;  
    }  
  
    @Validate()  // !
    setUsersInDatabase(@Positive() users: number): void {  // !
        this._users = users;  
    }  
}  
  
function Positive() {  // 1
    return (  
        target: Object, // UserService - класс  
        propertyKey: string | symbol, // setUsersInDatabase - метод  
        parameterIndex: number, // 0 - первый среди аргументов  
    ) => {  
        console.log(Reflect.getOwnMetadata('design:type', target, propertyKey));  
        console.log(Reflect.getOwnMetadata('design:paramtypes', target, propertyKey));  
        console.log(Reflect.getOwnMetadata('design:returntype', target, propertyKey));  
  
        // Добавляем символ позитивности в метаданных для аргументов  
        let existParams: number[] = Reflect.getOwnMetadata(POSITIVE_METADATA_KEY, target, propertyKey) || [];  
        existParams.push(parameterIndex);  
        Reflect.defineMetadata(POSITIVE_METADATA_KEY, existParams, target, propertyKey)  
    }  
}  
  
function Validate() {  // !
    return (  
        target: Object,  
        propertyKey: string | symbol,  
        descriptor: TypedPropertyDescriptor<(...args: any[]) => any>  
    ) => {  
        // Уже тут валидируем, помеченные как позитивные, данные  
        let method = descriptor.value;  
        descriptor.value = (...args: any) => {  
            let positiveParams: number[] = Reflect.getOwnMetadata(POSITIVE_METADATA_KEY, target, propertyKey) || [];  
            if (positiveParams) {  
                for (let index of positiveParams) {  
                    if (args[index] < 0 ) {  
                        throw new Error('Number must be bigger then 0');  
                    }  
                }  
            }  
            return method?.apply(this, args);  
        }    }  
}  
  
const userService = new UserService();  
console.log(userService.setUsersInDatabase(10));  
console.log(userService.setUsersInDatabase(-1));

093 Порядок декораторов

  • Первыми у нас всегда срабатывают свойства (вне зависимости от положения в коде).
  • Дальше срабатывает инициализация метода и его параметров. А уже вызов происходит в обратном порядке.
  • Дальше происходит тот же самый порядок со статиками.
  • И уже в конце инициализируется класс с его параметром. В обратном порядке они выполняются

Порядок расположения кода влияет только на одинаковые уровни инициализации - они выполняются по порядку в коде

function Uni(name: string): any {  
    console.log(`Инициализация: ${name}`);  
    return function () {  
        console.log(`Вызов: ${name}`);  
    }  
}  
  
@Uni('Класс')  
class MyClass {  
    @Uni('Свойство')  
    props?: any;  
  
    @Uni('Статическое свойство')  
    static prop?: any;  
  
    @Uni('Метод')  
    method(@Uni('Параметр') _: string) {  }  
  
    @Uni('Статический метод')  
    static methodStatic(@Uni('Параметр статического метода') _: string) {  }  
  
    constructor(@Uni('Параметр конструктора') _: string) {  
    }
}