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 2023
Dekoratory 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
.
{
"compilerOptions": {
"target": "es6",
"experimentalDecorators": true
}
}
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.
function Logger(target: Function) {
console.log('Logging...')
console.log(target)
}
@Logger
class Agent {
name = 'Sam'
constructor() {
console.log('Creating a Third Echelon agent')
}
}
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.
function Logger(message: string) {
return function (constructor: Function) {
console.log(message)
console.log(constructor)
}
}
@Logger('Logging in agent...')
class Agent {
name = 'Sam'
constructor() {
console.log('Creating a Third Echelon agent')
}
}
@Logger('Logging in civilian...')
class Civilian {
name = 'Sarah'
constructor() {
console.log('Creating a civilian')
}
}
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.
function Greeting(constructor: Function) {
console.log('Hello!')
}
function Logger(constructor: Function) {
console.log('Logging...')
}
@Logger
@Greeting
class Agent {
name = 'Sam'
constructor() {
console.log('Creating a Third Echelon agent')
}
}
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.
function FirstDecorator(constructor: Function) {
console.log('This message will be logged first')
}
function SecondDecorator(constructor: Function) {
console.log('This message will be logged second')
}
@SecondDecorator
@FirstDecorator
class Agent {
name = 'Sam'
constructor() {
console.log('Creating a Third Echelon agent')
}
}
“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.
function FirstDecoratorFactory() {
console.log('Message from this factory will be logged first')
return function (constructor: Function) {
console.log('Message from this decorator will be logged second')
}
}
function SecondDecoratorFactory() {
console.log('Message from this factory will be logged second')
return function (constructor: Function) {
console.log('Message from this decorator will be logged first')
}
}
@FirstDecoratorFactory()
@SecondDecoratorFactory()
class Agent {
name = 'Sam'
constructor() {
console.log('Creating a Third Echelon agent')
}
}
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.
function Log(target: any, propertyName: string | symbol) {
console.log('Property Decorator')
console.log(target, propertyName)
}
class Agent {
@Log
name: string
constructor() {
console.log('Creating a Third Echelon agent')
}
}
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.
function Log(
target: any,
propertyName: string | symbol,
descriptor: PropertyDescriptor
) {
console.log('Accessor Decorator')
console.log(target)
console.log(name)
console.log(descriptor)
}
class Agent {
private _name: string
@Log
set name(value: string) {
this._name = value
}
constructor() {
console.log('Creating a Third Echelon agent')
}
}
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.
function Log(
target: any,
name: string | symbol,
descriptor: PropertyDescriptor
) {
console.log('Method Decorator')
console.log(target)
console.log(name)
console.log(descriptor)
}
class Agent {
name = 'Sam'
@Log
getName() {
return this.name
}
constructor() {
console.log('Creating a Third Echelon agent')
}
}
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.
function Log(target: any, name: string | symbol, parameterIndex: number) {
console.log('Parameter Decorator')
console.log(target)
console.log(name)
console.log(position)
}
class Agent {
name = 'Sam'
greetings(@Log name: string) {
console.log(`Hello ${name}!`)
}
constructor() {
console.log('Creating a Third Echelon agent')
}
}
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.
@Component({
selector: 'app-agent',
inputs: ['name'],
template: ` Agent Name: {{ agentName }} `
})
export class AgentComponent {
name: string | null = null
}
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.
import { Controller, Get } from '@nestjs/common'
@Controller('/agents')
export class AgentController {
@Get()
findAll(): string {
return 'This action returns all agents'
}
}
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.