Dekoratory

Funkcje przypinane znakiem @ do klas i ich elementów, które rozszerzają albo zmieniają ich działanie.

Zwraca
Dla klasy, metody i akcesora: element, który zastąpi oryginał, albo nic (bez zmian). Dekorator pola może zwrócić funkcję przekształcającą wartość początkową.
Na tej stronie

Przykład

#
TypeScript
function logged<This, Args extends unknown[], Result>(
  method: (this: This, ...args: Args) => Result,
  context: ClassMethodDecoratorContext<This>
) {
  const name = String(context.name)
  return function (this: This, ...args: Args): Result {
    console.log(`LOG: ${name}(${args.join(', ')})`)
    return method.apply(this, args)
  }
}

class ScoreBoard {
  private total = 0

  @logged
  add(points: number) {
    this.total += points
    return this.total
  }
}

const board = new ScoreBoard()
board.add(20)
console.log(board.add(15))
Wynik
LOG: add(20)
LOG: add(15)
35

Definicja i zastosowanie

#

Dekorator to zwykła funkcja, którą przypinasz znakiem @ do klasy albo jej elementu, np. @logged nad metodą. Uruchamia się raz, w chwili definiowania klasy, i dostaje dekorowany element oraz obiekt context z jego opisem. Jeśli zwróci nową funkcję, zastąpi ona oryginalną metodę. Raz napisaną logikę, np. logowanie, pomiar czasu czy sprawdzanie uprawnień, dołączysz wtedy do dowolnej metody jedną linijką.

TypeScript od wersji 5.0 obsługuje standardowe dekoratory z propozycji TC39, bez żadnej dodatkowej opcji. Można nimi oznaczać klasy, metody, gettery i settery, pola oraz pola z modyfikatorem accessor. Nie działają przy zwykłych funkcjach ani przy parametrach, gdzie kompilator zgłasza „Decorators are not valid here”. Przy target niższym niż ESNext kompilator zamienia dekoratory na zwykły JavaScript z funkcjami pomocniczymi.

Wiele popularnych bibliotek, np. NestJS i TypeORM, korzysta ze starszej, eksperymentalnej wersji dekoratorów, włączanej opcją experimentalDecorators. Takie dekoratory dostają inne argumenty (obiekt docelowy, nazwę i deskryptor właściwości), mogą oznaczać parametry i współpracują z opcją emitDecoratorMetadata. Obie wersje są niezgodne: dekorator napisany dla jednej nie zadziała w drugiej.

Składnia

#
Składnia
function dekorator(value, context) {
  return zamiennik // opcjonalnie
}

@dekorator
class Klasa {
  @dekorator metoda() { … }
  @dekorator pole = 1
}

Argumenty dekoratora

#
  • value

    zależy od elementu

    Dekorowany element: klasa, metoda, getter albo setter, a przy polu accessor obiekt z get i set. Przy zwykłych polach to undefined, bo w chwili definiowania klasy pole nie ma jeszcze wartości.
  • context

    ClassMethodDecoratorContext i pokrewne

    Opis elementu: kind (rodzaj, np. "method" albo "field"), name (nazwa), static, private oraz metoda addInitializer(), która rejestruje kod uruchamiany przy inicjalizacji.

Więcej przykładów

#
Dekorator z argumentami
TypeScript
function clamp(min: number, max: number) {
  return function (_value: undefined, _context: ClassFieldDecoratorContext<unknown, number>) {
    return (initial: number) => Math.min(max, Math.max(min, initial))
  }
}

class ReaderSettings {
  @clamp(12, 24) fontSize = 40
  @clamp(1, 3) columns = 0
}

console.log(new ReaderSettings())
Wynik
ReaderSettings { fontSize: 24, columns: 1 }
Dekorator klasy
TypeScript
const registry: string[] = []

function register(_value: new (...args: any[]) => object, context: ClassDecoratorContext) {
  registry.push(String(context.name))
}

@register
class QuizTask {}

@register
class CodeTask {}

console.log(registry)
Wynik
['QuizTask', 'CodeTask']

Dobre praktyki

#
  • Dekorator z argumentami, np. @clamp(12, 24), to funkcja, która zwraca właściwy dekorator. Tak zbudowana jest większość dekoratorów z bibliotek.
  • Dekorator pola zmienia tylko wartość początkową. Żeby kontrolować każdy późniejszy zapis, oznacz pole słowem accessor i użyj dekoratora, który podmieni jego get i set.
  • Sprawdź, czy tsconfig.json nie ma experimentalDecorators: true. Wtedy obowiązują stare sygnatury i przykłady z tej strony nie przejdą kompilacji.

Powiązane hasła

#

Widzisz błąd albo brakuje przykładu? Napisz do nas.