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:
| Dekoruje | value | Typ kontekstu | Zwraca |
|---|---|---|---|
| klasę | klasę | ClassDecoratorContext | klasę zastępczą albo nic |
| metodę | metodę | ClassMethodDecoratorContext | metodę zastępczą |
| getter / setter | getter albo setter | ClassGetterDecoratorContext / ClassSetterDecoratorContext | zastępczy getter albo setter |
| pole | undefined | ClassFieldDecoratorContext | funkcję, 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ów | nieobsługiwane (TS1206) | obsługiwane |
emitDecoratorMetadata | nieobsługiwane | obsługiwane (informacje o typach w runtime przez reflect-metadata) |
Dekoratory accessor ({ get, set, init }), addInitializer | tak | nie |
| Oparte na | propozycji TC39 | jej 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.