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