Ograniczenie generyczne zawęża, czym może być parametr typu. <T extends { length: number }> przyjmuje tylko typy z liczbowym length (napisy, tablice, twoje własne obiekty), a dzięki tej obietnicy funkcja może odczytać .length.
Typem zwracanym jest nadal T, a nie { length: number }: przekazanie dwóch tablic liczb zwraca number[], a obiekty zachowują swoje tag. Ograniczenie opisuje minimum; typ wywołującego zostaje zachowany.
Po co są ograniczenia
Wewnątrz funkcji generycznej nieograniczone T może być czymkolwiek: napisem, liczbą, null, funkcją. TypeScript pozwala więc tylko na to, co działa dla każdego typu, czyli prawie na nic.
Kompilator wypisuje:
index.ts(3,12): error TS2339: Property 'length' does not exist on type 'T'.
index.ts(3,24): error TS2339: Property 'length' does not exist on type 'T'.
Rozwiązanie to powiedzieć, czego potrzebujesz: <T extends { length: number }>. Ograniczenia zamieniają „dowolny typ” w „dowolny typ, który ma to”, a zwykle właśnie to ma na myśli funkcja generyczna.
Ograniczenie do interfejsu
Ograniczeniem może być dowolny typ, także interfejs lub alias typu, który już masz. Funkcja działa wtedy ze wszystkim, co ma co najmniej te składowe, i nadal zwraca pełny typ wywołującego.
Porównaj ze zwykłym typem parametru: function byId(items: HasId[], id: number): HasId | undefined przyjmuje te same tablice, ale wynik ma tylko typ HasId, więc found.name byłoby błędem. Wersja generyczna przekazuje pełny typ dalej.
K extends keyof T: bezpieczny dostęp do właściwości
Najczęstsze ograniczenie wiąże dwa parametry typu. K extends keyof T mówi, że K musi być jedną z nazw właściwości T, a T[K] to typ tej właściwości.
Literówka lub brakujący klucz to błąd kompilacji, a typ zwracany podąża za kluczem: "year" daje number, "title" daje string. Samo keyof, łącznie z jego zachowaniem przy sygnaturach indeksu, opisuje strona o keyof.
Ograniczenie do prymitywów
Ograniczeniem może być prymityw albo ich unia. Przydatny efekt uboczny: gdy T extends string (lub number), TypeScript wnioskuje literalny typ argumentu, zamiast go poszerzać.
Bez ograniczenia createEvent("user.saved") zwraca { type: string }: TypeScript poszerza wtedy literał wywnioskowany dla T do string. Z T extends string literał zostaje zachowany i w ten sposób otypowane helpery zdarzeń i routingu wyciągają dokładne nazwy ze zwykłych argumentów napisowych.
Typowe błędy
Zwracanie czegoś, co pasuje tylko do ograniczenia. Funkcja, która zwraca T, musi zwrócić T, a nie dowolną wartość pasującą do ograniczenia:
interface HasId {
id: number;
}
function reset<T extends HasId>(item: T): T {
return { id: item.id };
}
// error TS2322: Type '{ id: number; }' is not assignable to type 'T'.
// '{ id: number; }' is assignable to the constraint of type 'T', but 'T' could be
// instantiated with a different subtype of constraint 'HasId'.
Jeśli T to { id: number; name: string }, nowy obiekt nie ma name, więc nie jest T. Zwróć rozwinięcie wejścia, return { ...item, id: 0 };, które zachowuje każdą właściwość, albo zadeklaruj typ zwracany jako HasId, jeśli tylko tyle tworzysz.
Przekazanie argumentu typu, który łamie ograniczenie. Jawne argumenty typu też są sprawdzane: przy function scale<T extends number>(x: T) wywołanie scale<string>("2") kończy się błędem Type 'string' does not satisfy the constraint 'number'. (TS2344).
Ograniczenie tam, gdzie generyk nie jest potrzebny. Jeśli T występuje tylko w parametrze, function print<T extends HasId>(item: T): void to po prostu function print(item: HasId): void z dodatkową składnią. Ograniczenia mają znaczenie, gdy T pojawia się też w typie zwracanym albo w innym parametrze.
Ograniczenia z wartościami domyślnymi
Parametr typu może mieć jednocześnie ograniczenie i wartość domyślną. Wartość domyślna musi spełniać ograniczenie.
interface Store<TState extends object = Record<string, unknown>> {
get(): TState;
set(next: Partial<TState>): void;
}
type AnyStore = Store; // Store<Record<string, unknown>>
type CounterStore = Store<{ count: number }>; // fine
type BadStore = Store<number>;
// error TS2344: Type 'number' does not satisfy the constraint 'object'.
To samo słowo kluczowe extends pojawia się też w conditional types, T extends string ? A : B, gdzie jest testem, a nie ograniczeniem; tę formę opisuje strona o conditional types.
Najczęściej zadawane pytania
Co znaczy T extends w generyku TypeScript?
<T extends Constraint> oznacza, że T może być dowolnym typem, który da się przypisać do Constraint. Wywołujący mogą przekazać tylko takie typy, a wewnątrz funkcji możesz używać wszystkiego, co gwarantuje ograniczenie. <T extends { length: number }> przyjmuje napisy, tablice i każdy obiekt z liczbowym length i pozwala odczytać value.length.
Co znaczy K extends keyof T?
K musi być jedną z nazw właściwości T. Razem z typem dostępu indeksowanego T[K] dokładnie typuje dostęp do właściwości: function get<T, K extends keyof T>(obj: T, key: K): T[K] przyjmuje tylko prawdziwe klucze obj, a typem zwracanym jest typ tej właściwości.
Skąd błąd "Property does not exist on type T"?
Nieograniczone T może być dowolnym typem, również number albo null, więc TypeScript nie pozwala na nim na nic specyficznego dla typu (TS2339). Dodaj ograniczenie, które obiecuje tę właściwość: <T extends { name: string }>, albo ogranicz do interfejsu, który już masz, <T extends User>.
Co znaczy "T could be instantiated with a different subtype of constraint"?
Błąd TS2322: funkcja obiecuje zwrócić T, a zwraca wartość, która pasuje tylko do ograniczenia. Jeśli T to { id: number; name: string }, nowe { id: 1 } nie jest T. Zwróć otrzymaną wartość (albo jej rozwinięcie z odpowiednim typem) lub zmień typ zwracany na ograniczenie.
Czym różni się extends w generyku od extends w klasie lub interfejsie?
To samo słowo kluczowe, pokrewna idea. W class Dog extends Animal i interface B extends A deklaruje dziedziczenie. W <T extends A> deklaruje ograniczenie: T musi dać się przypisać do A. W conditional type, T extends A ? X : Y, jest testem, który wybiera jeden z dwóch typów.