Антон Ларичев
Введение
Сегодня мы более детально поговорим о декораторах в 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);При этом декораторы могут стекаться друг на друга. Например, мы можем поставить один декоратор над другим, и тогда первый декоратор обернёт функцию, а второй декоратор обернёт результат выполнения первого декоратора. И это большое преимущество декораторов - мы можем комбинировать их для создания более сложной функциональности.



Комментарии
0