Menu

Dekoratory w TypeScript: metody, klasy, pola i akcesory

Dekoratory to funkcje, które opakowują albo zastępują składowe klas za pomocą składni @. Poznaj standardowe dekoratory, które TypeScript obsługuje bez żadnej flagi (klasy, metody, gettery, pola i akcesory), fabryki dekoratorów, addInitializer i różnice względem starszego experimentalDecorators używanego przez Angular i NestJS.

Na tej stronie są działające edytory: edytuj, uruchamiaj i od razu zobacz wynik.

Dekorator to funkcja, którą dołączasz do klasy albo składowej klasy za pomocą @name. Dostaje oryginalną metodę (albo klasę, albo pole) oraz opisujący ją obiekt kontekstu i może zwrócić zamiennik. TypeScript obsługuje standardowe dekoratory bez żadnej flagi kompilatora.

@logged wykonuje się raz, gdy klasa zostaje zdefiniowana, i zastępuje add zwracanym opakowaniem. Każde wywołanie przechodzi przez to opakowanie. Parametry generyczne zachowują typy this, argumentów i wartości zwracanej metody, więc add nadal przyjmuje dwie liczby i zwraca liczbę.

Jak kompilują się dekoratory

Standardowe dekoratory pochodzą z propozycji TC39 dla JavaScriptu, a TypeScript implementuje je od wersji 5.0. Propozycja nie jest jeszcze częścią standardu JavaScript, a Node 24 nie parsuje składni @, więc przy targecie ES2022 (jak na tych stronach) kompilator przepisuje każdą dekorowaną klasę na zwykły JavaScript, który wywołuje funkcje pomocnicze (__esDecorate i __runInitializers, emitowane na początku pliku). Wynik działa wszędzie tam, gdzie działa ES2022.

Jedna konsekwencja: pliku z dekoratorami nie da się uruchomić przy wbudowanym w Node usuwaniu typów (node file.ts), które usuwa tylko typy i zostawia @ na miejscu. Node zatrzymuje się z SyntaxError: Invalid or unexpected token. Najpierw skompiluj plik przez tsc albo bundler.

Rodzaje dekoratorów i ich sygnatury

Każdy standardowy dekorator ma kształt (value, context) => replacement | void. To, czym jest value i co możesz zwrócić, zależy od tego, co jest dekorowane:

DekorujevalueTyp kontekstuZwraca
klasęklasęClassDecoratorContextklasę zastępczą albo nic
metodęmetodęClassMethodDecoratorContextmetodę zastępczą
getter / settergetter albo setterClassGetterDecoratorContext / ClassSetterDecoratorContextzastępczy getter albo setter
poleundefinedClassFieldDecoratorContextfunkcję, która przekształca wartość początkową
pole accessor{ get, set }ClassAccessorDecoratorContext{ get?, set?, init? }

Każdy obiekt kontekstu ma kind, name i addInitializer. Kontekst składowej klasy ma też static, private i obiekt access do odczytu składowej z instancji. Dekoratory działają także na składowych statycznych i #private.

Fabryki dekoratorów

Żeby przekazać opcje, napisz funkcję, która zwraca dekorator, i wywołaj ją w miejscu @. To fabryka dekoratorów:

@retry(3) najpierw wywołuje retry, a zwracana przez nią funkcja jest właściwym dekoratorem. Wiele dekoratorów można układać jeden na drugim: w @a @b method() najpierw stosowany jest b, a a opakowuje wynik.

Dekoratory klas i addInitializer

Dekorator klasy dostaje samą klasę. Może zwrócić podklasę, która ją zastąpi, albo nic nie zwracać i tylko gdzieś ją zapisać. context.addInitializer rejestruje kod do wykonania w określonym momencie: dla dekoratora klasy zaraz po pełnym zdefiniowaniu klasy; dla dekoratora metody przy tworzeniu każdej instancji.

Dekorator klasy stoi nad klasą (albo po export). Bez @bound wywołanie loose() rzuciłoby wyjątek, bo this byłoby undefined.

Dekoratory pól i akcesorów

Dekorator pola nie widzi późniejszych przypisań i nie może ich przechwycić: jego value to undefined, a jedyne, co może zwrócić, to funkcja przekształcająca wartość początkową pola. Żeby przechwytywać odczyty i zapisy, zadeklaruj pole ze słowem kluczowym accessor, które zamienia je w parę getter i setter opartą na prywatnym magazynie, i udekoruj właśnie je:

accessor należy do tej samej propozycji. Emituje prawdziwy getter i setter nad polem #private, dlatego p.price = -5 przechodzi przez set dekoratora.

Standardowe dekoratory a starsze experimentalDecorators

Przed TypeScript 5.0 jedynymi dekoratorami w TypeScript była wczesna wersja propozycji, włączana przez experimentalDecorators. Ta flaga nadal istnieje i przełącza kompilator na starszy model, z innymi sygnaturami i semantyką:

Standardowe (bez flagi)Starsze (experimentalDecorators)
Sygnatura(value, context)(target, propertyKey, descriptor)
Jak zmienia metodęzwraca nową funkcjęmodyfikuje descriptor.value
Dekoratory parametrównieobsługiwane (TS1206)obsługiwane
emitDecoratorMetadatanieobsługiwaneobsługiwane (informacje o typach w runtime przez reflect-metadata)
Dekoratory accessor ({ get, set, init }), addInitializertaknie
Oparte napropozycji TC39jej starszym szkicu

Dekorator napisany dla jednego modelu nie przechodzi sprawdzania typów w drugim. Oto dekorator w starym stylu w projekcie bez flagi:

index.ts(11,5): error TS1241: Unable to resolve signature of method decorator when called as an expression.
  The runtime will invoke the decorator with 2 arguments, but the decorator expects 3.

Rozwiązanie to albo przepisanie go w standardowej formie (value, context), jak w pierwszym przykładzie na tej stronie, albo włączenie starszego modelu dla całego projektu:

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

Angular, NestJS i TypeORM nadal ustawiają experimentalDecorators w konfiguracjach generowanych przez swoje narzędzia i wymagają go w dokumentacji. NestJS i TypeORM potrzebują też emitDecoratorMetadata, bo odczytują typy w czasie działania: NestJS, żeby wstrzykiwać parametry konstruktora, a TypeORM, żeby mapować właściwości na kolumny:

// Legacy model: needs experimentalDecorators (and emitDecoratorMetadata for DI).
@Injectable()
class UsersService {
    constructor(@Inject(DB) private db: Database) {}
}

Jeśli używasz jednego z tych frameworków, pisz dekoratory w starym stylu i trzymaj się jego dokumentacji. W nowym kodzie bez takiego frameworka używaj standardowych dekoratorów.

Kiedy używać dekoratorów

Dekoratory pasują do zachowań przekrojowych, które inaczej powtarzałyby się w wielu metodach: logowania, pomiaru czasu, cache, ponawiania prób, sprawdzania uprawnień, walidacji i rejestrowania klas w kontenerze albo routerze. Ukrywają też przepływ sterowania, bo czytelnik musi sprawdzić, co robi @retry, zanim zrozumie, co robi metoda. Przy jednym albo dwóch użyciach prostsza jest zwykła funkcja wyższego rzędu (const fetchData = retry(3, rawFetch)), która działa też poza klasami.

Najczęściej zadawane pytania

Czym są dekoratory w TypeScript?

Dekorator to funkcja stosowana do klasy albo składowej klasy za pomocą składni @name. Dostaje dekorowany element i obiekt kontekstu, a może zwrócić zamiennik: opakowaną metodę, nową klasę albo funkcję, która przekształca początkową wartość pola. Typowe zastosowania to logowanie, walidacja, cache i rejestrowanie klas.

Czy do dekoratorów w TypeScript potrzebuję experimentalDecorators?

Nie. Od TypeScript 5.0 standardowe dekoratory (TC39) działają bez żadnej flagi. experimentalDecorators przełącza kompilator na starszy model dekoratorów, na którym zbudowane są frameworki takie jak Angular i NestJS. Oba modele używają innych sygnatur funkcji i nie są wymienne.

Czym różnią się dekoratory standardowe od experimentalDecorators?

Dekoratory standardowe dostają (value, context) i zwracają zamiennik. Starsze dekoratory dostają (target, propertyKey, descriptor) i modyfikują deskryptor właściwości. Tylko starszy model obsługuje dekoratory parametrów i emitDecoratorMetadata; tylko model standardowy ma dekoratory accessor, które zwracają { get, set, init }, oraz context.addInitializer.

Czy TypeScript obsługuje dekoratory parametrów?

Tylko przy włączonym experimentalDecorators. W trybie standardowym dekorator na parametrze to błąd TS1206: Decorators are not valid here, bo propozycja TC39 nie obejmuje dekoratorów parametrów. Frameworki do wstrzykiwania zależności, które dekorują parametry konstruktora, potrzebują więc starszej flagi.

W jakiej kolejności stosowane są dekoratory?

Wyrażenia dekoratorów są obliczane od góry do dołu, ale stosowane od dołu do góry: w @a @b method() najpierw b opakowuje metodę, a potem a opakowuje wynik. Dlatego przy wywołaniu metody a działa jako zewnętrzna warstwa.

Ilustracja języków programowania w Coddy

Ucz się programowania z Coddy

ZACZNIJ