JS Tower
JavaScript & TypeScriptМодуль 8: Продвинутый TypeScript

Декораторы

Декораторы классов и методов

Цель урока

В этом уроке ты научишься:

  • Создавать декораторы классов
  • Создавать декораторы методов
  • Использовать декораторы свойств

Экспериментальная функция

Декораторы — экспериментальная функция. Включи experimentalDecorators в tsconfig.json.


Настройка

{
  "compilerOptions": {
    "experimentalDecorators": true,
    "emitDecoratorMetadata": true
  }
}

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

function Logger(constructor: Function) {
  console.log("Создан класс: " + constructor.name);
}

@Logger
class User {
  constructor(public name: string) {}
}

// При загрузке: "Создан класс: User"

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

function Logger(prefix: string) {
  return function(constructor: Function) {
    console.log(prefix + ": " + constructor.name);
  };
}

@Logger("LOG")
class User {
  constructor(public name: string) {}
}
// "LOG: User"

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

function Log(
  target: any,
  propertyKey: string,
  descriptor: PropertyDescriptor
) {
  const original = descriptor.value;
  
  descriptor.value = function(...args: any[]) {
    console.log(`Вызов ${propertyKey} с аргументами:`, args);
    const result = original.apply(this, args);
    console.log(`Результат:`, result);
    return result;
  };
}

class Calculator {
  @Log
  add(a: number, b: number): number {
    return a + b;
  }
}

let calc = new Calculator();
calc.add(2, 3);
// Вызов add с аргументами: [2, 3]
// Результат: 5

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

function MinLength(length: number) {
  return function(target: any, propertyKey: string) {
    let value: string;
    
    const getter = () => value;
    const setter = (newValue: string) => {
      if (newValue.length < length) {
        throw new Error(`${propertyKey} должен быть минимум ${length} символов`);
      }
      value = newValue;
    };
    
    Object.defineProperty(target, propertyKey, {
      get: getter,
      set: setter
    });
  };
}

class User {
  @MinLength(3)
  name: string;
  
  constructor(name: string) {
    this.name = name;
  }
}

let user = new User("Иван"); // OK
// let user2 = new User("Ив"); // Ошибка!

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

function Required(
  target: any,
  propertyKey: string,
  parameterIndex: number
) {
  console.log(`Параметр ${parameterIndex} в ${propertyKey} обязателен`);
}

class User {
  greet(@Required name: string) {
    console.log("Привет, " + name);
  }
}

Порядок выполнения

function First() {
  console.log("First factory");
  return function(target: any) {
    console.log("First decorator");
  };
}

function Second() {
  console.log("Second factory");
  return function(target: any) {
    console.log("Second decorator");
  };
}

@First()
@Second()
class Example {}

// First factory
// Second factory
// Second decorator (снизу вверх)
// First decorator

Практический пример

function Autobind(
  _target: any,
  _propertyKey: string,
  descriptor: PropertyDescriptor
) {
  const original = descriptor.value;
  
  return {
    configurable: true,
    enumerable: false,
    get() {
      return original.bind(this);
    }
  };
}

class Button {
  label = "Нажми меня";
  
  @Autobind
  handleClick() {
    console.log(this.label);
  }
}

let button = new Button();
let handler = button.handleClick;
handler(); // "Нажми меня" (this привязан)

Практика

Задание 1: Декоратор класса

Задача: Создай декоратор, добавляющий timestamp.

Loading...
Ваш вывод:

Задание 2: Декоратор метода

Задача: Создай декоратор для измерения времени.

Loading...
Ваш вывод:

Проверь себя

  1. Как включить декораторы?
  2. В каком порядке выполняются декораторы?
  3. Какие типы декораторов существуют?