TypeScript - dekoratory
Udekorujmy nasz tor... kod używając dekoratorów TypeScript. Dekoratory są smacznym dodatkiem do klas i zapewniają składnię do metaprogramowania.
Zaktualizowano: 21 października 2023Dekoratory to eksperymentalna funkcjonalność w TypeScripcie. Zostały dodane w TypeScript 5.0 i mogą być dołączone do 5 różnych rzeczy: deklaracji klasy, metody, akcesora, właściwości i parametru. Zatem dotyczą głównie klasy. Możesz je wykorzystywać w metaprogramowaniu - technice, w której program ma wiedzę o samym sobie, albo może sobą manipulować. Nie mają one bezpośredniego wpływu na końcowego użytkownika. Jest to dobrze dostrojony instrument do pisania kodu, który jest łatwy do użycia przez innych programistów. Dostrójmy nasze instrumenty mózgowe, aby nauczyć się dekoratorów!
W momencie pisania, dekoratory są nadchodzącą funkcjonalnością w ECMAScript. Są na etapie 3 i mogą zostać dodane do natywnego JavaScriptu. Istnieje paragraf opisujący specyfikację ECMAScript w jednym z moich poprzednich postów.
Konfiguracja
Zanim zaczniemy się uczyć dekoratorów, mamy trochę pracy z konfiguracją. Aby uruchomić tą eksperymentalną funkcjonalność, masz dwie opcje.
Możesz dodać opcję do TypeScript CLI.
🔴 🟡 🟢
tsc --target ES6 --experimentalDecorators
Lub możesz dodać ją do tsconfig.json
.
JSON
1{2 "compilerOptions": {3 "target": "es6",4 "experimentalDecorators": true5 }6}
Definiowanie dekoratorów
Mając formalności za sobą, możemy przejść do dekoratorów. Dekorator, w zasadzie, to jest standardowa funkcja JavaScript. Funkcja, którą możesz zaaplikować do czegoś. Aby zaaplikować dekorator, potrzebujesz znaku małpy (@). TypeScript wykorzystuje ten symbol, aby rozpoznać dekorator.
TYPESCRIPT
1function Logger(target: Function) {2 console.log('Logging...')3 console.log(target)4}56@Logger7class Agent {8 name = 'Sam'910 constructor() {11 console.log('Creating a Third Echelon agent')12 }13}
Dekoratory, tak jak funkcje, mogą otrzymywać argumenty. Argumenty zależą od typu dekoratora. Dekorator klasy otrzymuje jeden, target
- jest to konstruktor klasy. Jeżeli dekorator zwraca wartość, zastępuje ona deklarację klasy. Później przejdziemy przez pozostałe typy dekoratorów.
Dekoratory zwykle wykorzystują PascalCase (lub UpperCamelCase) - powinny rozpoczynać się wielką literą.
Fabryka dekoratorów
Możemy zagnieździć kolejną funkcję w naszym dekoratorze, i w ten sposób, dostaniemy fabrykę dekoratorów. Fabryka dekoratorów zwraca funkcję dekoratora. Wykorzystując ten wzorzec, możemy skonfigurować dekorator przed dodaniem go do czegoś. Jeżeli odczuwasz tu funkcje wyższego rzędu, to twoja intuicja cię nie myli.
TYPESCRIPT
1function Logger(message: string) {2 return function (constructor: Function) {3 console.log(message)4 console.log(constructor)5 }6}78@Logger('Logging in agent...')9class Agent {10 name = 'Sam'1112 constructor() {13 console.log('Creating a Third Echelon agent')14 }15}1617@Logger('Logging in civilian...')18class Civilian {19 name = 'Sarah'2021 constructor() {22 console.log('Creating a civilian')23 }24}
Możemy wykorzystać parametry, aby posyłać wartości do tego wewnętrznego dekoratora. Daje nam to elastyczność w konfigurowaniu co dekorator robi wewnętrznie. W ten sposób możemy wygenerować wiele podobnych, ale różnych dekoratorów.
Wiele dekoratorów
“A co z dodawaniem większej ilość dekoratorów? Możemy to zrobić?”. Tak, możemy dodać więcej niż jeden dekorator do klasy. Albo gdziekolwiek dekoratory się aplikują. Składnia jest prosta - układaj dekoratory jeden na drugim, jak naleśniki o poranku.
TYPESCRIPT
1function Greeting(constructor: Function) {2 console.log('Hello!')3}45function Logger(constructor: Function) {6 console.log('Logging...')7}89@Logger10@Greeting11class Agent {12 name = 'Sam'1314 constructor() {15 console.log('Creating a Third Echelon agent')16 }17}
To rodzi kolejne pytanie - w jakiej kolejności są uruchamiane? Uruchamiane są od dołu do góry. Najniższy dekorator jako pierwszy, a później dekoratory powyżej.
TYPESCRIPT
1function FirstDecorator(constructor: Function) {2 console.log('This message will be logged first')3}45function SecondDecorator(constructor: Function) {6 console.log('This message will be logged second')7}89@SecondDecorator10@FirstDecorator11class Agent {12 name = 'Sam'1314 constructor() {15 console.log('Creating a Third Echelon agent')16 }17}
“No dobra, a co z fabrykami? Czy one też działają od dołu do góry?”. W zasadzie… nie. Przeciwnie - uruchamiają się od góry do dołu - w standardowej kolejności wykonywania. Może być to mylące, dlatego napisałem wiersz, który pomoże nam zapamiętać:
Na górze róże, na dole fiołki,
dekoratory lecą od dołu do góry,
a fabryki robią odwrotne fikołki.
TYPESCRIPT
1function FirstDecoratorFactory() {2 console.log('Message from this factory will be logged first')34 return function (constructor: Function) {5 console.log('Message from this decorator will be logged second')6 }7}89function SecondDecoratorFacotry() {10 console.log('Message from this factory will be logged second')1112 return function (constructor: Function) {13 console.log('Message from this decorator will be logged first')14 }15}1617@FirstDecoratorFactory()18@SecondDecoratorFacotry()19class Agent {20 name = 'Sam'2122 constructor() {23 console.log('Creating a Third Echelong agent')24 }25}
Kiedy wykonują się dekoratory?
Skoro dotykamy wykonywanie, złapmy ten temat i sprawdźmy kiedy właściwie dekoratory się uruchamiają. Jeżeli przyjrzysz się poprzednim fragmentom kodu, zauważysz, że żadna z powyższych klas nie została zainicjalizowana. Niemniej, zobaczysz wiadomości w konsoli. Dzieje się tak dlatego, ponieważ dekoratory (bez względu na typ) wykonują się kiedy definiujesz klasę. Nie kiedy ją inicjalizujesz. Nie są wykonywane w trakcie wykonywania programu. Pozwalają na dodanie zakulisowej pracy konfiguracyjnej kiedy klasa jest zdefiniowana.
Typy dekoratorów
Wspomniałem o różnych typach dekoratorów. Mamy opcje jeżeli chodzi o dodawanie dekoratorów. Wcześniej widzieliśmy dekoratory klasy. Ale nie musimy ich aplikować bezpośrednio do klas. Dekoratory możemy dodać np. do właściwości. To jakie argumenty dekorator otrzyma zależy od miejsca, w którym został dodany.
Podobna historia wiąże się ze zwracanymi wartościami. Niektóre dekoratory mają możliwość zwrócenia czegoś. To co otrzymasz z powrotem zależy od typu dekoratora, z którym pracujesz. Jednakże, tylko w niektórych dekoratorach zwracana wartość jest respektowana. Przejdźmy przez różne typy dekoratorów.
Dekoratory właściwości
Dekorator właściwości otrzymuje dwa argumenty:
target
- może być zarówno:- Funkcją konstruktora klasy - dla statycznego członka.
- Prototypem klasy - dla członka instancji.
propertyKey
- nazwa właściwości.
Zwracana wartość będzie ignorowana.
TYPESCRIPT
1function Log(target: any, propertyName: string | symbol) {2 console.log('Property Decorator')3 console.log(target, propertyName)4}56class Agent {7 @Log8 name: string910 constructor() {11 console.log('Creating a Third Echelon agent')12 }13}
Dekoratory akcesora
Aby uzyskać dostęp do właściwości, możemy wykorzystać akcesory. Do akcesorów również możemy dodać dekoratory. Argumenty, które otrzymują to:
target
- może być zarówno:- Funkcją konstruktora klasy - dla statycznego członka.
- Prototypem klasy - dla członka instancji.
propertyKey
- nazwa właściwości.descriptor
- deskryptor właściwości dla członka. Deskryptor w akcesorze ma następujące opcje:get
set
enumerable
configurable
Zwracana wartość może być wykorzystana jako deskryptor członka, jeżeli zostanie zwrócona.
TYPESCRIPT
1function Log(2 target: any,3 propertyName: string | symbol,4 descriptor: PropertyDescriptor5) {6 console.log('Accessor Decorator')7 console.log(target)8 console.log(name)9 console.log(descriptor)10}1112class Agent {13 private _name: string1415 @Log16 set name(value: string) {17 this._name = value18 }1920 constructor() {21 console.log('Creating a Third Echelon agent')22 }23}
Dekoratory metody
Dekoratory metod są podobne do tych z akcesorów. Główną różnicą są opcje deskryptora:
target
- może być zarówno:- Funkcją konstruktora klasy - dla statycznego członka.
- Prototypem klasy - dla członka instancji.
propertyKey
- nazwa właściwości.descriptor
- deskryptor właściwości dla członka. Możemy wykorzystać ten parametr, aby nadpisać oryginalną implementację i wstrzyknąć dodatkową logikę. Deskryptor w metodzie ma następujące opcje:value
writable
enumerable
configurable
Zwracana wartość może być wykorzystana jako deskryptor członka, jeżeli zostanie zwrócona.
TYPESCRIPT
1function Log(2 target: any,3 name: string | symbol,4 descriptor: PropertyDescriptor5) {6 console.log('Method Decorator')7 console.log(target)8 console.log(name)9 console.log(descriptor)10}1112class Agent {13 name = 'Sam'1415 @Log16 getName() {17 return this.name18 }1920 constructor() {21 console.log('Creating a Third Echelon agent')22 }23}
Dekoratory parametru
Możesz nawet dodać dekoratory do indywidualnych parametrów w metodzie. Ten typ dekoratora również przyjmuje trzy argumenty:
target
- może być zarówno:- Funkcją konstruktora klasy - dla statycznego członka.
- Prototypem klasy - dla członka instancji.
propertyKey
- nazwa właściwości. Ale tu mała uwaga - jest to nazwa metody. Nie nazwa parametru.parameterIndex
- pozycja parametru w liście parametrów funkcji.
Zwracana wartość będzie ignorowana.
TYPESCRIPT
1function Log(target: any, name: string | symbol, parameterIndex: number) {2 console.log('Parameter Decorator')3 console.log(target)4 console.log(name)5 console.log(position)6}78class Agent {9 name = 'Sam'1011 greetings(@Log name: string) {12 console.log(`Hello ${name}!`)13 }1415 constructor() {16 console.log('Creating a Third Echelon agent')17 }18}
Aby podsumować dekoratory, przygotowałem tabelę:
Dekorator | Klasy | Właściwości | Akcesora | Metody | Parametru |
---|---|---|---|---|---|
Argument 1 | target | target | target | target | target |
Argument 2 | propertyKey | propertyKey | propertyKey | propertyKey | |
Argument 3 | descriptor | descriptor | parameterIndex | ||
Zwracana wartość | Deklaracja klasy | Ignorowana | Deskryptor | Deskryptor | Ignorowana |
Przykłady dekoratorów
Angular
Jednym z popularniejszych frameworków, który wykorzystuje TypeScript jest Angular. Google zdecydowało, że Angular 2 będzie bazował na TypeScripcie, wymieniając statyczne typowanie jako jeden z powodów. Statyczne typy to zdecydowana zaleta, ale skupmy się na dekoratorach. Sprawdźmy jak Angular wykorzystuje dekoratory.
TYPESCRIPT
1@Component({2 selector: 'app-agent',3 inputs: ['name'],4 template: ` Agent Name: {{ agentName }} `5})6export class AgentComponent {7 name: string | null = null8}
Powyższy przykład reprezentuje rozpoznawalny dekorator Component
. Dekorator ten oznacza klasę jako komponent Angulara. Otrzymuje on konfigurację i metadane, determinujące jak ten podstawowy blok UI powinien wyglądać i się zachowywać podczas wykonywania programu. Możesz posłać tam template HTML, stylowanie czy schematy.
NestJS
NestJS to framework Node.js do budowania skalowalnych aplikacji po stronie serwera z wykorzystaniem języka TypeScript. Jest popularny pośród Back-end deweloperów. NestJS również polega na dekoratorach. Korzystając z NestJS, możesz zauważyć dekoratory w kontrolerach.
TYPESCRIPT
1import { Controller, Get } from '@nestjs/common'23@Controller('/agents')4export class AgentController {5 @Get()6 findAll(): string {7 return 'This action returns all agents'8 }9}
Kontrolery są odpowiedzialne za obsługę przychodzących zapytań i zwracanie odpowiedzi do klientów. Taki dekorator zarejestruje klasę w metadanych jako kontroler dla konkretnej trasy HTTP. Zatem dekoratory w NestJS są wykorzystywane m.in. jako mechanizm rutowania.
Podsumowanie
Pomimo że dekoratory nie wpływają na użytkownika końcowego, wpływają na jakość pracy programistów. Możesz wykorzystać je do obserwowania zmian wartości, transformowania parametrów, walidacji w trakcie wykonywania programu i na wiele innych sposobów. Zapewniają one elegancką składnie do modyfikowania lub rozszerzania zachowań klasy. Od niedawna TypeScript oferuje tę składnię jako część języka. Sprawdź moje poprzednie posty, aby dowiedzieć się więcej o OOP lub ogólnie o TypeScripcie.