keyof bierze typ obiektowy i tworzy unię nazw jego właściwości. Poniżej keyof User to typ "id" | "name" | "email", więc zmienna tego typu może przechowywać tylko jeden z tych trzech stringów.
keyof istnieje tylko w systemie typów. Jest usuwany podczas kompilacji, więc nigdy niczego nie sprawdza w czasie działania. Daje za to gwarancję w czasie kompilacji, że klucz nazywa prawdziwą właściwość: zmień nazwę email w interfejsie, a każde "email" otypowane jako keyof User stanie się błędem.
keyof typeof: klucze zwykłego obiektu
keyof działa na typach, a nie na wartościach. Dla obiektu zapisanego jako wartość najpierw pobierz jego typ przez typeof, a potem weź klucze:
Czytaj to od środka: typeof colors to { red: string; green: string; blue: string }, a keyof z tego to unia kluczy. Zapis keyof colors bez typeof kończy się błędem error TS2749: 'colors' refers to a value, but is being used as a type here. Did you mean 'typeof colors'?.
To typowy sposób na wyprowadzenie unii z tablicy wyszukiwania: dodajesz kolor do obiektu, a typ Color rośnie razem z nim. typeof ma też drugie znaczenie, w czasie działania; strona o typeof opisuje oba.
Typowany dostęp do właściwości z generykami
Najczęściej keyof występuje jako ograniczenie generyczne. K extends keyof T mówi „K jest jednym z kluczy T”, a T[K] to typ właściwości pod tym kluczem:
Drugi parametr typu ma znaczenie. Przy key: keyof T bez K typ zwracany to T[keyof T], czyli unia typów wszystkich właściwości (tutaj string | number | boolean), więc title.toUpperCase() kończyłoby się błędem error TS2339: Property 'toUpperCase' does not exist on type 'string | number | boolean'. Przechwycenie dokładnego klucza w K zachowuje dokładny typ właściwości. T[K] to indexed access type, opisany na stronie o indexed access types.
Indeksowanie zwykłym stringiem
Bardzo częsty błąd pojawia się przy indeksowaniu obiektu stringiem, który przyszedł skądinąd:
Kompilator wypisuje:
index.ts(10,10): error TS7053: Element implicitly has an 'any' type because expression of type 'string' can't be used to index type 'Settings'.
No index signature with a parameter of type 'string' was found on type 'Settings'.
Do read może trafić dowolny string, także "colour", a Settings nic o takim kluczu nie mówi. Są dwa rozwiązania: przyjmować tylko prawdziwe klucze albo sprawdzać nieznany string przed użyciem.
name is keyof Settings to predykat typu: gdy funkcja zwraca true, TypeScript traktuje input wewnątrz if jako klucz. Predykaty szczegółowo wyjaśnia strona o type guards.
keyof z sygnaturami indeksu: string | number
Dla typu z sygnaturą indeksu keyof zwraca typ indeksu, a sygnatura indeksu typu string daje string | number, a nie samo string:
JavaScript zamienia każdy liczbowy klucz właściwości na string, więc scores[42] i scores["42"] to ta sama właściwość. TypeScript odwzorowuje to, dodając number. Wynik pokazuje też regułę JavaScriptu: klucze wyglądające jak liczby całkowite są wypisywane jako pierwsze, rosnąco, przed pozostałymi kluczami typu string. Gdy funkcja generyczna potrzebuje tylko kluczy typu string, napisz Extract<keyof T, string> albo string & keyof T; Exclude i Extract mają własną stronę.
Wyniki keyof w skrócie
| Typ | keyof daje |
|---|---|
{ id: number; name: string } | "id" | "name" |
{ [key: string]: number } | string | number |
{ [index: number]: string } | number |
{} | never |
any | string | number | symbol |
{ a: 1; b: 2 } | { a: 3; c: 4 } | "a" (klucze, które ma każdy element) |
{ a: 1 } & { c: 4 } | "a" | "c" (klucze z każdej strony) |
string[] | number oraz nazwę każdej metody tablicy ("length", "push", ...) |
Wiersze z unią i przecięciem na początku wyglądają na odwrócone. Wartość typu unii może być dowolnym z elementów, więc bezpieczne są tylko klucze obecne w obu. Wartość przecięcia ma wszystko z obu stron, więc ma wszystkie klucze.
Dlaczego Object.keys zwraca string[]
Object.keys(obj) ma typ string[], nigdy (keyof T)[]. Powodem jest typowanie strukturalne: wartość może mieć więcej właściwości niż jej zadeklarowany typ.
p ma typ Point, a mimo to w czasie działania ma klucz z. Gdyby Object.keys obiecywało ("x" | "y")[], kod na tym polegający byłby dla p błędny. Rzutowanie jest bezpieczne dla literałów obiektowych tworzonych samodzielnie; dla obiektów przychodzących z zewnątrz zostaw klucze jako string i je sprawdzaj.
keyof w mapped types
keyof pozwala też przejść po kluczach typu, żeby zbudować nowy typ. Tak są napisane wbudowane utility types Partial i Readonly:
type MyPartial<T> = { [K in keyof T]?: T[K] };
type MyReadonly<T> = { readonly [K in keyof T]: T[K] };
interface User {
id: number;
name: string;
}
type UserDraft = MyPartial<User>; // { id?: number; name?: string }
[K in keyof T] odwiedza każdy klucz, a T[K] odczytuje typ właściwości pod tym kluczem. Mapped types mają własną stronę, z modyfikatorami i przemapowywaniem kluczy.
Najczęściej zadawane pytania
Co robi keyof w TypeScript?
keyof T tworzy unię nazw właściwości typu T. Dla interface User { id: number; name: string } keyof User to "id" | "name". Działa tylko na typach, w czasie kompilacji, i nie generuje żadnego JavaScriptu.
Czym jest keyof typeof w TypeScript?
keyof potrzebuje typu, a zwykły obiekt, taki jak const colors = { red: "#f00" }, jest wartością. typeof colors zamienia wartość w jej typ, a keyof typeof colors daje unię jego kluczy, tutaj "red". Zapis keyof colors kończy się błędem TS2749.
Dlaczego keyof zwraca string | number?
Dzieje się tak, gdy typ ma sygnaturę indeksu typu string, na przykład { [key: string]: number }. JavaScript zamienia liczbowe klucze właściwości na stringi, więc obj[42] też jest poprawnym dostępem, a TypeScript dodaje number do typu kluczy. Użyj Extract<keyof T, string> albo string & keyof T, jeśli chcesz tylko klucze typu string.
Dlaczego Object.keys zwraca string[], a nie (keyof T)[]?
Bo obiekt może mieć więcej właściwości, niż wymienia jego typ: wartość z dodatkowym polem da się przypisać do węższego typu. Otypowanie wyniku jako (keyof T)[] byłoby dla takich obiektów kłamstwem. Jeśli wiesz, że obiekt ma dokładnie zadeklarowane klucze, zrób rzutowanie: Object.keys(obj) as (keyof typeof obj)[].
Jak naprawić "Element implicitly has an 'any' type because expression of type 'string' can't be used to index type"?
Indeks to zwykły string, a typ obiektu nie ma sygnatury indeksu typu string. Otypuj parametr jako keyof YourType zamiast string albo najpierw sprawdź string przez type guard, który zawęża go do keyof YourType, albo daj obiektowi sygnaturę indeksu lub typ Record<string, V>, jeśli naprawdę przyjmuje dowolny klucz.