TypeScript nie ma osobnej klasy słownika ani hashmapy. Słownik to albo zwykły obiekt otypowany sygnaturą indeksu, { [key: string]: number }, albo ten sam typ zapisany jako Record<string, number>, albo Map<string, number>. Wszystkie trzy przechowują wartości po kluczu; różnią się typami kluczy, sposobem typowania brakujących kluczy i serializacją.
Dla kluczy napisowych i danych w kształcie JSON zwykłym wyborem jest obiekt z Record<string, T>. Dla kluczy innych niż napisy albo wpisów ciągle dodawanych i usuwanych użyj Map.
Sygnatury indeksu
Sygnatura indeksu, [key: KeyType]: ValueType, mówi „każdy klucz tego typu wskazuje wartość tego typu”. Nazwa klucza (key, name, userId) to tylko dokumentacja. Typy kluczy mogą być string, number, symbol, wzorcami szablonów literałów albo uniami tych typów.
JavaScript zamienia klucze liczbowe na napisy, więc obiekt { [id: number]: string } nadal ma w czasie działania klucze napisowe: Object.keys({ 1: "one" }) to [ '1' ]. Liczbowa sygnatura indeksu ogranicza tylko to, jak możesz go indeksować w TypeScript.
Record<K, V>
Record<string, V> to skrót od { [key: string]: V }. Z unią kluczy literałowych zamiast string staje się stałym słownikiem, który musi zawierać każdy klucz:
Pominięcie staging w urls to błąd kompilacji TS2741 (Property 'staging' is missing...), dzięki czemu Record z kluczami w postaci unii to sprawdzana tablica wyszukiwania. Partial sprawia, że każda wartość ma typ number | undefined.
Map jako hashmapa
Map przyjmuje klucze dowolnego typu, zachowuje kolejność wstawiania, ma size i uczciwie typuje brakujące klucze: get zwraca V | undefined.
Sprawdzanie, czy klucz istnieje
Istnieje kilka sprawdzeń i nie wszystkie znaczą to samo:
| Sprawdzenie | Działa na | Uważaj na |
|---|---|---|
Object.hasOwn(obj, key) | obiektach | ES2022; na starszych targetach użyj Object.prototype.hasOwnProperty.call(obj, key) |
key in obj | obiektach | Prawdziwe także dla odziedziczonych kluczy, takich jak toString i constructor |
obj[key] !== undefined | obiektach | Nie odróżnia brakującego klucza od klucza z zapisanym undefined |
if (obj[key]) | obiektach | Fałszywe także dla wartości 0, "" i false |
map.has(key) | Map | Nie zawęża późniejszego map.get(key) |
map.get(key) !== undefined | Map | To samo zastrzeżenie o undefined co przy obiektach |
Problem z odziedziczonymi kluczami sprawia, że klucze podawane przez użytkowników, takie jak "constructor" albo "__proto__", czynią zwykłe obiekty ryzykownymi jako słowniki. Map nie ma takich kluczy.
Problem z typem brakującego klucza
Na sygnaturze indeksu albo Record<string, T> odczyt dowolnego klucza ma typ T, nawet klucza, który nie istnieje. Kompilator pozwala wywoływać metody na wartości, która w czasie działania jest undefined:
Opcja kompilatora noUncheckedIndexedAccess to naprawia: z nią colors["grass"] ma typ string | undefined, a wywołanie toUpperCase to błąd kompilacji, dopóki go nie sprawdzisz. Nie jest częścią strict, więc trzeba ją włączyć osobno w tsconfig.json; inne flagi obok niej opisuje strona o trybie strict. Map nie ma tej luki, bo get zawsze uwzględnia undefined.
Dodawanie, usuwanie i iteracja
delete działa na właściwościach z sygnatury indeksu. Na wymaganej nazwanej właściwości typu obiektowego to błąd kompilacji TS2790, The operand of a 'delete' operator must be optional.
Czego użyć
| Potrzeba | Użyj |
|---|---|
| Klucze napisowe, JSON na wejściu lub wyjściu | Record<string, T> |
| Stały, znany zbiór kluczy, wszystkie wymagane | Record<"a" | "b", T> |
| Nazwane właściwości plus dowolne dodatkowe klucze | typ obiektowy z sygnaturą indeksu |
| Klucze będące obiektami, liczbami trzymanymi jako liczby albo czymkolwiek innym niż napis | Map<K, V> |
| Częste dodawanie i usuwanie albo często odczytywany rozmiar | Map<K, V> |
| Klucze pochodzące od użytkowników | Map<K, V> (brak odziedziczonych kluczy) |
Najczęściej zadawane pytania
Jak utworzyć słownik w TypeScript?
Otypuj zwykły obiekt sygnaturą indeksu, const ages: { [name: string]: number } = {}, albo równoważnym Record<string, number>. Potem dodawaj wpisy przez ages["ada"] = 36. Dla kluczy, które nie są napisami, albo kolekcji, do której często dodajesz i z której często usuwasz, użyj new Map<string, number>().
Czy TypeScript ma HashMap?
Nie pod tą nazwą. Wbudowany w JavaScript Map to hashmapa: Map<K, V> przechowuje pary klucz-wartość z szybkim wyszukiwaniem po kluczu, zachowuje kolejność wstawiania i przyjmuje klucze dowolnego typu. Zwykły obiekt otypowany jako Record<string, V> to drugi popularny wybór dla kluczy napisowych.
Jak sprawdzić, czy klucz istnieje w słowniku TypeScript?
Dla słownika w postaci obiektu użyj Object.hasOwn(dict, key) albo key in dict (które widzi też odziedziczone właściwości, takie jak toString), albo odczytaj wartość i porównaj ją z undefined. Dla Map użyj map.has(key) albo sprawdź bezpośrednio wynik map.get(key), bo has nie zawęża późniejszego get.
Czym różni się sygnatura indeksu od Record?
{ [key: string]: T } i Record<string, T> opisują ten sam typ. Record jest krótszy i może też przyjąć unię konkretnych kluczy, Record<"a" | "b", T>, która wymaga każdego klucza. Sygnaturę indeksu można połączyć z nazwanymi właściwościami w jednym typie obiektowym, a jej klucz może mieć nazwę, która go dokumentuje, jak w { [userId: string]: User }.
Dlaczego odczyt brakującego klucza słownika nie daje błędu?
Domyślnie dict[key] na sygnaturze indeksu albo Record<string, T> ma typ T, choć w czasie działania wartość dla brakującego klucza to undefined. Włącz noUncheckedIndexedAccess w tsconfig.json, a typ zmieni się na T | undefined, co wymusi sprawdzenie. strict nie obejmuje tej opcji.