Menu

Generyki w TypeScript: funkcje, typy i klasy generyczne

Generyki pozwalają funkcji, interfejsowi, typowi lub klasie działać z wieloma typami i zachować między nimi powiązanie: to, co wchodzi, decyduje o tym, co wychodzi. Poznaj funkcje generyczne, wnioskowanie argumentów typu, kilka parametrów typu, generyczne interfejsy i klasy, wartości domyślne i sytuacje, w których generyki są zbędne.

Na tej stronie są działające edytory: edytuj, uruchamiaj i od razu zobacz wynik.

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 parametruPrzyjmuje każde wejścieTyp wynikuSprawdzane
anytakanynie: błędy się kompilują
unknowntakunknowntak, ale przed użyciem trzeba zawęzić
T (generyk)takpowiązany z wejściemtak, 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.

Ilustracja języków programowania w Coddy

Ucz się programowania z Coddy

ZACZNIJ