Mapped type buduje nowy typ obiektowy, przechodząc w pętli po zestawie kluczy. { [K in keyof T]: boolean } znaczy "dla każdego klucza K z T właściwość o nazwie K i typie boolean":
Dodaj klucz do Features, a Flags<Features> też będzie go wymagać. Mapped types istnieją tylko w czasie kompilacji: opisują obiekty, ale ich nie tworzą.
Składnia
Mapped type ma trzy części: zmienną klucza, unię kluczy, po której się iteruje, i typ właściwości, który może używać zmiennej klucza.
type MappedType = {
[K in Keys]: PropertyType; // K takes each member of Keys in turn
};
Keys może być dowolną unią stringów, liczb lub symboli. Nie musi pochodzić z keyof:
Dokładnie to robi Record<Size, number>; Record to jednolinijkowy mapped type. Typ właściwości może używać klucza: { [K in keyof T]: T[K] } kopiuje typ każdej właściwości przez indexed access, a { [K in keyof T]: T[K] | null } dopuszcza null w każdej właściwości.
Modyfikatory: readonly i ? z + i -
Mapped type może dodać lub usunąć modyfikatory readonly i opcjonalności (?) we wszystkich właściwościach naraz. Przedrostek + dodaje (to domyślne, gdy nie piszesz żadnego), a - usuwa:
Ostatnia linia pokazuje, że readonly to tylko reguła czasu kompilacji: przypisanie zostało zgłoszone (i tu wyciszone przez @ts-expect-error), ale wygenerowany JavaScript i tak je wykonał. -? robi więcej niż usunięcie znaku zapytania: usuwa też undefined z typu właściwości, więc AllRequired<Account> odrzuca { id: 1, email: undefined } z komunikatem error TS2322: Type 'undefined' is not assignable to type 'string'.
Jak zbudowane są Partial, Readonly, Pick i Record
Wbudowane utility types, które zmieniają kształt obiektów, to mapped types. Oto definicje z pliku lib.es5.d.ts samego TypeScript:
type Partial<T> = { [P in keyof T]?: T[P] };
type Required<T> = { [P in keyof T]-?: T[P] };
type Readonly<T> = { readonly [P in keyof T]: T[P] };
type Pick<T, K extends keyof T> = { [P in K]: T[P] };
type Record<K extends keyof any, T> = { [P in K]: T };
Czytanie ich to dobre ćwiczenie: Pick iteruje tylko po przekazanych kluczach, a Record ignoruje typ źródłowy i daje każdemu kluczowi ten sam typ wartości. Omit nie jest osobnym mapped type: to Pick<T, Exclude<keyof T, K>>. Wszystkie są wymienione w przeglądzie utility types.
Zmiana nazw kluczy przez as
Klauzula as po kluczu zmienia nazwę właściwości. W połączeniu z template literal types może generować nowe nazwy ze starych:
string & K jest tu dlatego, że keyof T może zawierać klucze number i symbol, a Capitalize przyjmuje tylko stringi. Przecięcie zostawia klucze tekstowe i odrzuca resztę.
Filtrowanie kluczy przez never
Jeśli klauzula as daje never dla jakiegoś klucza, ten klucz zostaje usunięty. Z conditional type możesz zachować lub odrzucić właściwości na podstawie ich typu:
Typy takie jak DataOnly przydają się do opisania tego, co przetrwa JSON.stringify, albo tego, co edytuje formularz: pól bez metod.
Które modyfikatory są zachowywane
Mapped type po keyof T dla jakiegoś typu T nazywa się homomorficznym i kopiuje modyfikatory readonly oraz ? każdej właściwości z T. Mapped type po zwykłej unii kluczy zaczyna bez modyfikatorów:
type Account = { readonly id: number; email?: string };
type Copy<T> = { [K in keyof T]: T[K] };
type A = Copy<Account>;
// { readonly id: number; email?: string } (modifiers kept)
type B = { [K in "id" | "email"]: Account[K] };
// { id: number; email: string | undefined } (email is required now)
B wciąż ma undefined w typie email, bo Account["email"] go zawiera, ale sama właściwość nie jest już opcjonalna: { id: 1 } zostaje odrzucone. Dlatego Partial<T> i pokrewne typy mapują po keyof T: zachowują wszystko, czego jawnie nie zmieniają.
Generyczny homomorficzny mapped type zastosowany do tablicy lub krotki daje tablicę lub krotkę, a nie obiekt z kluczami liczbowymi. Readonly<string[]> to readonly string[], a przy type Box<T> = { [K in keyof T]: { value: T[K] } } typ Box<[string, number]> to [{ value: string }, { value: number }].
Mapped types a kod w czasie działania
Mapped type tylko opisuje obiekt, więc funkcja, która taki obiekt buduje, potrzebuje własnej pętli w czasie działania. Typ opisuje wtedy jej wynik:
Callbacki dostają typy parametrów z mapped type (v to string w jednym i number w drugim) bez żadnej adnotacji. Jeśli szukasz klasy Map, a nie mapowania typów, zobacz Map w TypeScript.
Najczęściej zadawane pytania
Czym jest mapped type w TypeScript?
To typ, który tworzy typ obiektowy, przechodząc po unii kluczy: { [K in Keys]: SomeType }. Najczęściej klucze to keyof T, więc nowy typ ma te same klucze co T, ale z przekształconymi typami właściwości, jak w type Flags<T> = { [K in keyof T]: boolean }.
Co znaczą +readonly, -readonly, +? i -? w mapped type?
Dodają lub usuwają modyfikatory. readonly albo +readonly robi każdą właściwość tylko do odczytu, -readonly usuwa readonly. ? albo +? robi każdą właściwość opcjonalną, -? robi je wymaganymi i usuwa też undefined z ich typów. Required<T> jest zapisany z -?.
Jak zmienić nazwy kluczy w mapped type?
Użyj klauzuli as po kluczu: { [K in keyof T as `get${Capitalize<string & K>}`]: () => T[K] } zamienia name na getName. Zmapowanie klucza na never w klauzuli as usuwa go i właśnie tak filtruje się właściwości.
Czy mapped type to to samo co Map w TypeScript?
Nie. Mapped type to przekształcenie typu w czasie kompilacji i nie generuje żadnego kodu. Map<K, V> to klasa Map z JavaScript, kolekcja par klucz-wartość w czasie działania. Wyszukiwania "typescript map type" często dotyczą jednego albo drugiego.
Jak zaimplementowane są Partial i Readonly?
Jako mapped types w bibliotece standardowej: type Partial<T> = { [P in keyof T]?: T[P] } i type Readonly<T> = { readonly [P in keyof T]: T[P] }. Pick i Record też są mapped types; Omit to Pick połączony z Exclude.