Generyki to parametry typu. Funkcja generyczna deklaruje typ zastępczy, zwykle nazywany T, a każde wywołanie go wypełnia, więc typy wejściowe decydują o typach wyjściowych. Jedna funkcja działa wtedy dla liczb, napisów i twoich własnych obiektów, a każde wywołanie jest nadal w pełni otypowane.
<T> po nazwie funkcji deklaruje parametr typu. items: T[] używa go dla wejścia, a T | undefined dla wyjścia, co wiąże jedno z drugim. Nigdzie nie napisano number ani string: TypeScript wywnioskował T z argumentu.
Dlaczego nie any albo unknown?
Bez generyków nadałbyś parametrowi typ any albo unknown. Oba przyjmują każdą tablicę i oba gubią powiązanie między wejściem a wyjściem:
| Typ parametru | Przyjmuje każde wejście | Typ wyniku | Sprawdzane |
|---|---|---|---|
any | tak | any | nie: błędy się kompilują |
unknown | tak | unknown | tak, ale przed użyciem trzeba zawęzić |
T (generyk) | tak | powiązany z wejściem | tak, z dokładnym typem |
Wnioskowanie argumentów typu
Zwykle TypeScript wnioskuje argumenty typu z przekazanych wartości. Możesz je też zapisać jawnie w nawiasach ostrych przy wywołaniu, co jest potrzebne, gdy żaden argument nie wspomina o T albo gdy wnioskowanie wybiera coś, czego nie chcesz.
Warto znać dwa wyniki wnioskowania. pair(1, "one") bez jawnego argumentu to błąd: TypeScript wnioskuje T = number z pierwszego argumentu, a potem zgłasza Argument of type 'string' is not assignable to parameter of type 'number'. (TS2345). Z kolei emptyList() bez żadnego argumentu dostaje T = unknown, co daje unknown[], a tego rzadko się chce.
Kilka parametrów typu
Funkcja może zadeklarować tyle parametrów typu, ile potrzebuje, oddzielonych przecinkami. Każdy jest wnioskowany niezależnie.
mapValues wnioskuje trzy rzeczy naraz: K to "tea" | "cake", V to number, a R to string z wartości zwracanej przez callback. K extends string to ograniczenie, opisane na następnej stronie.
Generyczne interfejsy i aliasy typów
Typy też mogą przyjmować parametry. Argument podajesz wtedy przy użyciu typu: Box<number>, ApiResponse<User>. Tak działa większość wbudowanych typów kolekcji: Array<T>, Map<K, V>, Promise<T>, Record<K, V>.
Result<T, E = string> pokazuje też domyślny parametr typu: Result<number> oznacza Result<number, string>. Tak jak opcjonalne parametry funkcji, parametry z wartościami domyślnymi muszą stać po wymaganych.
Klasy generyczne
Klasa przyjmuje parametry typu po swojej nazwie, a każda instancja je ustala. Mogą ich używać pola, metody i parametry konstruktora.
Jeśli konstruktor przyjmuje T, jawny argument można pominąć: new Box(5) wnioskuje Box<number>. Składowe statyczne należą do samej klasy, a nie do instancji, więc nie mogą używać parametru typu klasy (Static members cannot reference class type parameters., TS2302).
Generyczne funkcje strzałkowe i typy funkcji
Lista parametrów typu stoi przed listą parametrów. Ta sama składnia opisuje generyczny typ funkcji.
const last = <T>(items: T[]): T | undefined => items[items.length - 1];
type Mapper = <T, R>(items: T[], fn: (item: T) => R) => R[];
const mapAll: Mapper = (items, fn) => items.map(fn);
// In a .tsx file, <T> looks like a JSX tag. Add a trailing comma:
const lastTsx = <T,>(items: T[]) => items[items.length - 1];
Wnioskowanie typów literalnych przez parametry typu const
Domyślnie generyk wnioskuje poszerzone typy: pair("a", "b") daje [string, string]. Gdy liczą się dokładne wartości (nazwy tras, nazwy zdarzeń, listy kolumn), oznacz parametr jako const (TypeScript 5.0 i nowsze), a będzie wnioskował tak, jakby argument zapisano z as const:
Wywołujący pisze zwykłą tablicę, a funkcja zachowuje typy literalne.
Kiedy nie używać generyków
Parametr typu ma sens, gdy łączy dwie rzeczy: parametr i typ zwracany, dwa parametry albo parametr i callback. Jeśli T występuje tylko raz, nic nie wnosi i zwykły typ jest czytelniejszy.
// Pointless: T is used once, so it is just a longer way to write unknown
function logValue<T>(value: T): void {
console.log(value);
}
// Clearer
function logValueSimple(value: unknown): void {
console.log(value);
}
// Also pointless: returns T but nothing connects T to an argument,
// so the caller is really just asserting a type
function parseJson<T>(text: string): T {
return JSON.parse(text);
}
Ostatni przypadek jest częsty i mylący: parseJson<User>(text) wygląda na bezpieczne typowo, ale niczego nie sprawdza, dokładnie jak JSON.parse(text) as User. Zamiast tego zwróć unknown i zwaliduj dane. Inne oznaki nadużycia: parametr typu, który zawsze dostaje ten sam argument, albo taki, który dałoby się zastąpić unią dwóch znanych typów.
Dla generyków, które muszą coś wiedzieć o T (że ma length albo że K jest kluczem T), następnym krokiem są ograniczenia przez extends.
Najczęściej zadawane pytania
Czym są generyki w TypeScript?
Generyki to parametry typu: symbole zastępcze, takie jak T, które przy każdym użyciu funkcji, interfejsu, aliasu typu lub klasy wypełnia się prawdziwym typem. function first<T>(items: T[]): T | undefined działa dla każdej tablicy, a wynik ma typ elementu przekazanej tablicy, więc first([1, 2]) to number | undefined, a first(["a"]) to string | undefined.
Czym różnią się generyki od any?
any wyłącza sprawdzanie typów: wartość wejściowa i wyjściowa nie są ze sobą powiązane, a wynik też ma typ any. Generyk zachowuje to powiązanie: identity<T>(x: T): T zwraca dokładnie ten typ, który przekazano. Użyj unknown, gdy przyjmujesz cokolwiek, ale nie musisz oddawać typu, a generyka, gdy musisz.
Co znaczy <T> w TypeScript?
Deklaruje parametr typu o nazwie T. W function wrap<T>(value: T) T to zmienna typu, którą TypeScript przy każdym wywołaniu wypełnia na podstawie argumentu, albo którą przekazujesz jawnie jako wrap<string>("a"). Nazwa T to tylko konwencja; dłuższe nazwy, takie jak TItem czy Key, działają tak samo.
Jak napisać generyczną funkcję strzałkową w TypeScript?
Umieść listę parametrów typu przed parametrami: const first = <T>(items: T[]): T | undefined => items[0];. W pliku .tsx <T> zostałoby odczytane jako tag JSX, więc napisz <T,> z przecinkiem na końcu albo <T extends unknown>.
Jak ustawić domyślny typ generyka w TypeScript?
Dodaj = Type po parametrze: interface ApiResponse<T = unknown> { data: T }. Wtedy ApiResponse bez argumentu oznacza ApiResponse<unknown>. Parametry z wartościami domyślnymi muszą stać po tych bez nich, tak jak opcjonalne parametry funkcji.