Generics zapewniają fragmenty kodu wielokrotnego użytku, które działają z wieloma typami zamiast z pojedynczym typem. Generics zapewniają sposób traktowania typu jako zmiennej i określania go podczas użytkowania, podobnie jak parametry funkcji.
Generics mogą być używane w połączeniu z funkcjami (tworząc funkcję generyczną), klasami (tworząc klasę generyczną) i interfejsami (tworząc interfejs generyczny).
Użycie podstawowe
Prawdopodobnie używałeś generycznych w przeszłości, nawet o tym nie wiedząc - najczęstszym zastosowaniem generycznych jest deklarowanie tablicy:
const myArray: string[];
Na pierwszy rzut oka nie jest to nic specjalnego, po prostu deklarujemy myArray
jako tablica ciągów znaków, ale jest to to samo, co deklaracja generyczna:
const myArray: Array;
Zachowanie przejrzystości
Zacznijmy od bardzo prostego przykładu - jak moglibyśmy przenieść tę waniliową funkcję JS do TypeScript:
function getPrefiledArray(filler, length) {
return (new Array(length)).fill(filler);
}
Ta funkcja zwróci tablicę wypełnioną podaną ilością wypełniacz
więc długość
będzie liczba
a cała funkcja zwróci tablicę wypełniacz
- ale czym jest wypełniacz? W tym momencie może to być wszystko, więc jedną z opcji jest użycie dowolny
:
function getPrefiledArray(filler: any, length: number): any[] {
return (new Array(length)).fill(filler);
}
Korzystanie z dowolny
jest z pewnością generyczna - możemy przekazać dosłownie wszystko, więc "praca z wieloma typami zamiast pojedynczego typu" z definicji jest w pełni pokryta, ale tracimy połączenie między wypełniacz
i typ zwracany. W tym przypadku chcemy zwrócić pewną wspólną rzecz i możemy jawnie zdefiniować tę wspólną rzecz jako parametr typu:
function getPrefiledArray(filler: T, length: number): T[] {
return (new Array(length)).fill(filler);
}
i używać w ten sposób:
const prefilledArray = getPrefiledArray(0, 10);
Ogólne ograniczenia
Przyjrzyjmy się innym, prawdopodobnie bardziej powszechnym przypadkom. Dlaczego właściwie używamy typów w funkcjach? Dla mnie jest to zapewnienie, że argumenty przekazywane do funkcji będą miały pewne właściwości, z którymi chcę wchodzić w interakcje.
Jeszcze raz spróbujmy przenieść prostą waniliową funkcję JS do TS.
function getLength(thing) {
return thing.length;
}
Mamy do czynienia z nietrywialną zagadką - jak zapewnić, że dana rzecz posiada długość
i pierwszą myślą może być zrobienie czegoś takiego:
function getLength(thing: typeof Array):number {
return thing.length;
}
i w zależności od kontekstu może to być poprawne, ogólnie rzecz biorąc, jesteśmy nieco ogólnikowi - będzie działać z tablicami wielu typów, ale co, jeśli tak naprawdę nie wiemy, czy rzecz powinna zawsze być tablicą - może rzecz jest boiskiem piłkarskim lub skórką od banana? W takim przypadku musimy zebrać wspólne właściwości tej rzeczy w konstrukcję, która może definiować właściwości obiektu - interfejs:
interfejs IThingWithLength {
length: number;
}
Możemy użyć IThingWithLength
jako typ interfejsu rzecz
parametr:
function getLength(thing: IThingWithLength):number {
return thing.length;
}
Szczerze mówiąc, w tym prostym przykładzie będzie to całkowicie w porządku, ale jeśli chcemy zachować ten typ generyczny i nie napotkać problemu z pierwszego przykładu, możemy użyć Ogólne ograniczenia:
function getLength(thing: T):number {
return thing.length;
}
i używać go:
interfejs IBananaPeel {
grubość: liczba;
długość: liczba;
}
const bananaPeel: IBananaPeel = {grubość: 0.2, długość: 3.14};
getLength(bananaPeel);
Korzystanie z rozciąga się
zapewnia, że T
będzie zawierać właściwości zdefiniowane przez IThingWithLength
.
Klasy ogólne
Do tego momentu pracowaliśmy z funkcjami generycznymi, ale nie jest to jedyne miejsce, w którym generics błyszczą, zobaczmy, jak możemy je włączyć do klas.
Najpierw spróbujmy przechować kiść bananów w koszyku z bananami:
class Banana {
konstruktor(
public długość: liczba,
public color: string,
public ionizingRadiation: liczba
) {}
}
class BananaBasket {
private bananas: Banana[] = [];
add(banana: Banana): void {
this.bananas.push(banana);
}
}
const bananaBasket = new BananaBasket();
bananaBasket.add(new Banana(3.14, 'red', 10e-7));
Teraz spróbujmy stworzyć koszyk ogólnego przeznaczenia, do różnych rzeczy tego samego typu:
class Basket {
private stuff: T[] = [];
add(thing: T): void {
this.stuff.push(thing);
}
}
const bananaBasket = new Basket();
I na koniec, załóżmy, że nasz kosz jest pojemnikiem na materiały radioaktywne i możemy przechowywać tylko materię, która ma promieniowanie jonizujące
nieruchomości:
interfejs IRadioactive {
ionizingRadiation: number;
}
class RadioactiveContainer {
private stuff: T[] = [];
add(thing: T): void {
this.stuff.push(thing);
}
}
Interfejs ogólny
Na koniec spróbujmy zebrać całą naszą wiedzę i zbudować radioaktywne imperium również przy użyciu Generic Interfaces:
// Definiowanie wspólnych atrybutów dla kontenerów
interfejs IRadioactive {
ionizingRadiation: number;
}
// Definiuje coś, co jest radioaktywne
interface IBanana extends IRadioactive {
długość: liczba;
color: string;
}
// Zdefiniuj coś, co nie jest radioaktywne
interfejs IDog {
weight: number;
}
// Zdefiniuj interfejs dla kontenera, który może przechowywać tylko radioaktywne rzeczy
interface IRadioactiveContainer {
add(thing: T): void;
getRadioactiveness():number;
}
// Zdefiniuj klasę implementującą interfejs kontenera radioaktywnego
class RadioactiveContainer implements IRadioactiveContainer {
private stuff: T[] = [];
add(thing: T): void {
this.stuff.push(thing);
}
getRadioactiveness(): number {
return this.stuff.reduce((a, b) => a + b.ionizingRadiation, 0)
}
}
// BŁĄD! Typ "IDog" nie spełnia ograniczenia "IRadioactive
// I to jest trochę brutalne, aby przechowywać psy wewnątrz radioaktywnego kontenera
const dogsContainer = new RadioactiveContainer();
// Wszystko w porządku fam!
const radioactiveContainer = new RadioactiveContainer();
// Pamiętaj o sortowaniu odpadów radioaktywnych - utwórz osobny pojemnik tylko na banany
const bananasContainer = new RadioactiveContainer();
To wszystko!