21 października 2023 9 min. czytania

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
Babeczki z turkusowym lukrem udekorowane posypką
Zdjęcie autorstwa Brooke Lark

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.

JSON
1{
2 "compilerOptions": {
3 "target": "es6",
4 "experimentalDecorators": true
5 }
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}
5
6@Logger
7class Agent {
8 name = 'Sam'
9
10 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}
7
8@Logger('Logging in agent...')
9class Agent {
10 name = 'Sam'
11
12 constructor() {
13 console.log('Creating a Third Echelon agent')
14 }
15}
16
17@Logger('Logging in civilian...')
18class Civilian {
19 name = 'Sarah'
20
21 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}
4
5function Logger(constructor: Function) {
6 console.log('Logging...')
7}
8
9@Logger
10@Greeting
11class Agent {
12 name = 'Sam'
13
14 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}
4
5function SecondDecorator(constructor: Function) {
6 console.log('This message will be logged second')
7}
8
9@SecondDecorator
10@FirstDecorator
11class Agent {
12 name = 'Sam'
13
14 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')
3
4 return function (constructor: Function) {
5 console.log('Message from this decorator will be logged second')
6 }
7}
8
9function SecondDecoratorFacotry() {
10 console.log('Message from this factory will be logged second')
11
12 return function (constructor: Function) {
13 console.log('Message from this decorator will be logged first')
14 }
15}
16
17@FirstDecoratorFactory()
18@SecondDecoratorFacotry()
19class Agent {
20 name = 'Sam'
21
22 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}
5
6class Agent {
7 @Log
8 name: string
9
10 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: PropertyDescriptor
5) {
6 console.log('Accessor Decorator')
7 console.log(target)
8 console.log(name)
9 console.log(descriptor)
10}
11
12class Agent {
13 private _name: string
14
15 @Log
16 set name(value: string) {
17 this._name = value
18 }
19
20 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: PropertyDescriptor
5) {
6 console.log('Method Decorator')
7 console.log(target)
8 console.log(name)
9 console.log(descriptor)
10}
11
12class Agent {
13 name = 'Sam'
14
15 @Log
16 getName() {
17 return this.name
18 }
19
20 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}
7
8class Agent {
9 name = 'Sam'
10
11 greetings(@Log name: string) {
12 console.log(`Hello ${name}!`)
13 }
14
15 constructor() {
16 console.log('Creating a Third Echelon agent')
17 }
18}

Aby podsumować dekoratory, przygotowałem tabelę:

DekoratorKlasyWłaściwościAkcesoraMetodyParametru
Argument 1targettargettargettargettarget
Argument 2propertyKeypropertyKeypropertyKeypropertyKey
Argument 3descriptordescriptorparameterIndex
Zwracana wartośćDeklaracja klasyIgnorowanaDeskryptorDeskryptorIgnorowana

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 = null
8}

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'
2
3@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.

Newsletter, który rozpala ciekawość💡

Subskrybuj mój newsletter, aby otrzymywać comiesięczną dawkę:

  • Nowości, przykładów, inspiracji ze świata front-end, programowania i designu
  • Teorii naukowych i sceptycyzmu
  • Moich ulubionych źródeł, idei, narzędzi i innych interesujących linków
Nie jestem nigeryjskim księciem, aby oferować ci okazje. Nie wysyłam spamu. Anuluj kiedy chcesz.

Pozostań ciekawy. Przeczytaj więcej

Połowa winyla na białym tle1 września 20227 min. czytania

Dostępne animacje w React

Czyli jak nie kręcić swoimi użytkownikami (jak winylem). Niektóre animacje mogą powodować problemy u użytkoników. Zadbamy o nich i sprawimy, że nieistotne animacje będą opcjonalne.

Czytaj wpis
Zdjęcie zmiennych CSS w edytorze Visual Studio Code14 września 20227 min. czytania

Konwertowanie tokenów projektowych na zmienne CSS z Node.js

Konwertowanie tokenów projektowych jest procesem podatnym na błędy - przekonałem się o tym na własnej skórze. Dlatego stworzyłem prosty skrypt dla środowiska Node.js, który pomoże mi z tym zadaniem.

Czytaj wpis
Pięć metalowych kół zębatych na czarnym tle23 września 202210 min. czytania

Gatsby z Netlify CMS

W tym wpisie przyjrzymy się bliżej Netlify CMS. Jest to przykład CMSa nowego typu, który jest oparty o Git. Zintegrujemy go z przykładowym projektem Gatsby.

Czytaj wpis