логотип PurpleSchool

Новые декораторы в TypeScript 5.0

17 марта 2023 г.
2 423 просмотра
фото команды
Автор

Антон

Введение

Сегодня мы более детально поговорим о декораторах в TypeScript 5.0 и рассмотрим примеры их использования.

Подготовка проекта

Разберем настройку проекта, которая необходима для работы с декораторами:

  • Выполним инициализацию файла package.json с помощью команды npm init.
  • Используем команду tsc --init для инициализации файла tsconfig.json.
  • Затем установим TypeScript Beta в качестве зависимости:
    npm i -D typescript@beta
    TypeScript Beta установит в dev-зависимости TypeScript 5.0 Beta.

Важно: если на момент прочтения уже вышла стабильная версия, вы можете использовать ее.

Теперь нужно сделать сборку. Для этого переходим в tsconfig.json, находим параметр outDir и задаем его значение равным ./dist, чтобы все наши транспилированные файлы сохранялись внутри директории dist:

"outDir": "./dist",

В файле package.json создаем несколько скриптов:

  • Первый - это скрипт build, который отвечает за сборку приложения. Он будет использовать локально установленную версию TypeScript:
    json "build": "tsc",
  • Второй скрипт - это start, который запускает приложение:
    json "start": "node ./dist/app.js"

Кроме того, нам нужно создать файл app.ts, в котором мы будем экспериментировать.

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

Создадим класс Demo, который мы будем декорировать. У него будет метод exec, который принимает число и просто выводит это число с помощью console.log().

class Demo {
    exec(a: number) {
        console.log(a);
    }
}

Добавляем декоратор methodDec:

class Demo {

    @methodDec
    exec(a: number) {
        console.log(a);
    }
}

Все декораторы - это просто функции, которые всегда принимают два аргумента: target, который мы декорируем, и context - это контекст нашего декоратора. При этом декораторы, теоретически, могут быть универсальными, использоваться одновременно как на методе, так и на классе.

Для типизации добавим немного Generics: This - это ссылка на наш класс, Args - это массив аргументов типа any, а Return - это тип возвращаемого значения нашего метода:

function methodDec<This, Args extends any[], Return>(
    target: (this: This, ...args: Args) => Return,
    context
) {

}

Для context у нас есть ClassMethodDecoratorContext - это контекст декоратора:

function methodDec<This, Args extends any[], Return>(
    target: (this: This, ...args: Args) => Return,
    context: ClassMethodDecoratorContext<This, (this: This, ...args: Args) => Return> 
)

Сам декоратор метода возвращает функцию где мы может изменить поведение исходного метода:

class Demo {

    @methodDec 
    exec(a: number) {
        console.log(a);
    }
}

function methodDec<This, Args extends any[], Return>(
    target: (this: This, ...args: Args) => Return,
    context: ClassMethodDecoratorContext<This, (this: This, ...args: Args) => Return>
) {
     console.log('Init method');
     return function(this: This, ...args: Args): Return {
          const res = target.call(this, ...args);
         return res;
    }
}

const demo = new Demo();
demo.exec(1);

Чтобы проверить, что наш декоратор работает, необходимо ввести команды npm run build и npm start:

Init method
1

Пример - Декоратор Max number

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

Max - это функция, которая принимает число, больше которого аргумент передать нельзя:

class Demo {
    @Max(10)
    exec(a: number) {
        console.log(a);
    }
}

function Max(num: number) {
    return function <This, Args extends any[], Return>(
        target: (this: This, ...args: Args) => Return,
        context: ClassMethodDecoratorContext<This, (this: This, ...args: Args) => Return>
    ) {
          return function(this: This, ...args: Args): Return {
                if (args[0] > num) {
                    throw new Error(`Значение больше ${num}`)
                }
               const res = target.call(this, ...args);
              return res;
            }
    }
} 

const demo = new Demo();
demo.exec(1);
demo.exec(11);

После выполнения в во втором вызове получаем ошибку, так как значение больше 10.

При этом мы можем прописать совершенно любую логику, которая нам необходима, и обернуть функцию не только с одним, но и с несколькими аргументами, если это необходимо.

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

Декоратор класса у нас принимает target - это new (…args: Args) и возвращает наш This, а в качестве контекста он использует ClassDecoratorContext с описанием самого класса:

@classDec
class Demo {
    @Max(10)
    exec(a: number) {
        console.log(a);
    }
}

// код

function classDec<This, Args extends any[]>(
    target: new (...args: Args) => This,
    context: ClassDecoratorContext<new (...args: Args) => This>
) {
     console.log('Class init');
}

const demo = new Demo();
demo.exec(1);

Одно из частых использований такого декоратора, которое можно увидеть в библиотеках – это Dependency Injection.

Декоратор поля

У декоратора поля есть следующие параметры: target, у которого значение будет undefined, и context, у которого будет ClassFieldDecoratorContext.

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

Контекст принимает This и тип, в данном случае - string. В отличие от остальных, он может возвращать модифицированную функцию инициализации:

@classDec
class Demo {
    @fieldDec
    name: string = 'Test';

    @Max(10)
    exec(a: number) {
        console.log(a);
    }
}

// код

function fieldDec<This>(
    target: undefined,
    context: ClassFieldDecoratorContext<This, string>
) {
        console.log('Field init');
        return function (value: string) {
            console.log('Field init function')
            return value;
      }
}

// код

Мы можем модифицировать поведение только один раз при инициализации. Но на текущий момент не все функции декоратора доступны, поэтому в будущем будет возможно реализовать изменение поведение при get и set.

Декоратор setter

Очень похож на декоратор метода, но имеет другую типизацию ClassSetterDecoratorContext:

@classDec
class Demo {
    private _surname!: string;

    @fieldDec
    name: string = 'Test';

    @setDec
    set surname(value: string) {
        this._surname = value;
    }

    @Max(10)
    exec(a: number) {
        console.log(a);
    }
}

function setDec<This, Return>(
    target: (this: This, arg: any) => Return,
    context: ClassSetterDecoratorContext<This, (this: This, arg: any) => Return>
) {
     console.log('Init method');
     return function (this: This, arg: any): Return {
         const res = target.call(this, arg);
         return res;
     }
}

// код

Пример - setter проверка на строку

Сделаем декоратор @IsString:

@classDec
class Demo {
    private _surname!: string;

    @fieldDec
    name: string = 'Test';

    @IsString
    set surname(value: string) {
        this._surname = value;
    }

    @Max(10)
    exec(a: number) {
        console.log(a);
    }
}

// код

function IsString<This, Return>(
    target: (this: This, arg: any) => Return,
    context: ClassSetterDecoratorContext<This, (this: This, arg: any) => Return>
) {
     console.log('Set method');
     return function (this: This, arg: any): Return {
         if (typeof arg !== 'string') {
             throw new Error ('Не строка')
         }
         const res = target.call(this, arg);
         return res;
     }
}

// код

const demo = new Demo();
demo.exec(1);

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

изображение курса

TypeScript с нуля

Антон Ларичев
иконка часов18 часов лекций
иконка зведочки рейтинга4.8
TypeScript с нуля