Typy generyczne w TypeScript to potężne narzędzie, które pozwala na pisanie bardziej elastycznego, wielokrotnego użytku kodu. Generyki pozwalają tworzyć komponenty, które mogą działać z różnymi typami danych, zamiast być ograniczone do jednego konkretnego typu. Dzięki temu możemy pisać bardziej abstrakcyjne i uniwersalne funkcje, klasy czy interfejsy.
Czym są typy generyczne?
Typy generyczne umożliwiają definiowanie funkcji, klas lub interfejsów, które mogą działać z różnymi typami, niezależnie od tego, jaki dokładnie typ zostanie przekazany podczas ich użycia. Zamiast tworzyć osobną funkcję dla każdego typu danych, możemy zdefiniować jedną funkcję, która będzie działać z każdym typem.
Przykład:
Załóżmy, że chcemy stworzyć funkcję, która zwraca pierwszy element z tablicy. Możemy stworzyć funkcję dla tablic liczb i osobno dla tablic ciągów znaków, ale używając generyków, możemy to zrobić bardziej uniwersalnie.
Bez generyków:
function getFirstNumber(arr: number[]): number {
return arr[0];
}
function getFirstString(arr: string[]): string {
return arr[0];
}
Z generykami:
function getFirstElement(arr: T[]): T {
return arr[0];
}
W powyższym przykładzie T to symbol generyczny, który reprezentuje typ przekazany do funkcji. Dzięki temu funkcja getFirstElement działa z tablicami dowolnych typów.
Przykład użycia:
let numbers = [1, 2, 3];
let strings = ["jabłko", "banan", "gruszka"];
let firstNumber = getFirstElement(numbers); // Zwraca: 1
let firstString = getFirstElement(strings); // Zwraca: "jabłko"
Generyki w klasach
Tak jak funkcje, również klasy mogą być generyczne, co pozwala na tworzenie bardziej elastycznych struktur. Możemy zdefiniować generyczną klasę, która działa z różnymi typami, w zależności od tego, co zostanie przekazane podczas tworzenia instancji.
Przykład:
class Box
{
content: T;
constructor(content: T) {
this.content = content;
}
getContent(): T {
return this.content;
}
}
W tej klasie Box, typ T może być dowolnym typem, który zostanie określony podczas tworzenia instancji klasy.
Przykład użycia:
let numberBox = new Box(123);
let stringBox = new Box("Witaj!");
console.log(numberBox.getContent()); // Zwraca: 123
console.log(stringBox.getContent()); // Zwraca: "Witaj!"
Każda instancja klasy Box przechowuje wartości różnego typu, a kod jest bardziej elastyczny i skalowalny.
Generyki w interfejsach
Interfejsy również mogą być generyczne, co pozwala na tworzenie elastycznych struktur danych, które mogą działać z różnymi typami.
W tym przykładzie interfejs Pair ma dwa parametry generyczne: T i U. Dzięki temu możemy tworzyć różne kombinacje typów dla właściwości first i second.
Ograniczenia typów (Type Constraints)
Czasami chcemy ograniczyć generyki do określonych typów. Możemy to zrobić, stosując "ograniczenia typów", czyli za pomocą słowa kluczowego extends. Pozwala to na definiowanie generyków, które muszą spełniać określone warunki, np. implementować pewien interfejs.
Przykład:
function logLength<T extends { length: number }>(item: T): void {
console.log(item.length);
}
logLength("Hello"); // Działa, ponieważ string ma właściwość length
logLength([1, 2, 3]); // Działa, ponieważ tablica ma właściwość length
// logLength(123); // Błąd, liczba nie ma właściwości length
W powyższym przykładzie funkcja logLength działa tylko z typami, które mają właściwość length, co wyklucza np. liczby.
Domyślne typy generyczne
TypeScript pozwala również na definiowanie domyślnych wartości dla parametrów generycznych, jeśli użytkownik nie przekaże konkretnego typu. Można to osiągnąć, dodając domyślny typ podczas deklaracji.
Przykład:
function identity<T = string>(value: T): T {
return value;
}
let result1 = identity(10); // T to number
let result2 = identity(); // T to domyślnie string
W tym przykładzie, jeśli nie podamy typu, TypeScript domyślnie założy, że typ T to string.
Korzyści z generyków
Korzystanie z generyków w TypeScript ma wiele zalet:
Elastyczność: Pozwalają tworzyć wielokrotnego użytku funkcje, klasy i interfejsy, które działają z różnymi typami danych.
Bezpieczeństwo typów: TypeScript automatycznie sprawdza typy, co pomaga unikać błędów w trakcie kompilacji.
Przejrzystość kodu: Generyki pomagają w zachowaniu przejrzystości i czytelności kodu, szczególnie w większych projektach.
Podsumowanie
Typy generyczne to jedno z kluczowych narzędzi w TypeScript, które pozwalają na tworzenie bardziej elastycznego, zwięzłego i bezpiecznego kodu. Dzięki generykom możemy pisać funkcje, klasy i interfejsy, które mogą działać z różnymi typami danych, co zwiększa ich wielokrotność użycia i zmniejsza konieczność duplikacji kodu. Ograniczenia typów pozwalają na dodatkową kontrolę nad tym, jakiego rodzaju typy mogą być używane z generykami, co sprawia, że nasz kod staje się bardziej bezpieczny.