Wymuszanie typu indeksowanych elementów obiektu Typescript?

290

Chciałbym zapisać mapowanie ciągu -> ciąg w obiekcie Typescript i wymusić, aby wszystkie klucze były odwzorowane na ciągi. Na przykład:

var stuff = {};
stuff["a"] = "foo";   // okay
stuff["b"] = "bar";   // okay
stuff["c"] = false;   // ERROR!  bool != string

Czy istnieje sposób na wymuszenie, że wartości muszą być łańcuchami (lub jakimkolwiek typem ...)?

Armentaż
źródło

Odpowiedzi:

521
var stuff: { [key: string]: string; } = {};
stuff['a'] = ''; // ok
stuff['a'] = 4;  // error

// ... or, if you're using this a lot and don't want to type so much ...
interface StringMap { [key: string]: string; }
var stuff2: StringMap = { };
// same as above
Ryan Cavanaugh
źródło
13
Interesujące w tej składni jest to, że jakikolwiek typ inny niż „string” dla klucza jest w rzeczywistości nieprawidłowy. Ma to sens, biorąc pod uwagę, że mapy JS są jednoznacznie wpisywane przez ciągi, ale powoduje to, że składnia jest nieco zbędna. Coś w rodzaju „{}: ciąg”, aby po prostu określić typy wartości, wydawałoby się prostsze, chyba że zamierzają dodać w jakiś sposób, aby umożliwić automatyczne przymuszanie kluczy w ramach generowanego kodu.
Armentage
43
numberjest również dozwolony jako typ indeksowania
Ryan Cavanaugh
3
warto zauważyć: kompilator wymusza tylko typ wartości, a nie typ klucza. możesz robić rzeczy [15] = „cokolwiek” i to nie będzie narzekać.
amwinter
3
Nie, wymusza typ klucza. Nie możesz robić rzeczy [myObject] = 'cokolwiek', nawet jeśli myObject ma ładną implementację toString ().
AlexG,
7
@RyanCavanaugh Czy można (lub będzie) używać type Keys = 'Acceptable' | 'String' | 'Keys'jako typu indeksowania (klucza)?
calebboyd
131
interface AgeMap {
    [name: string]: number
}

const friendsAges: AgeMap = {
    "Sandy": 34,
    "Joe": 28,
    "Sarah": 30,
    "Michelle": "fifty", // ERROR! Type 'string' is not assignable to type 'number'.
};

Interfejs AgeMapwymusza klucze jako ciągi, a wartości jako liczby. Słowo kluczowe namemoże być dowolnym identyfikatorem i powinno być używane do sugerowania składni interfejsu / typu.

Możesz użyć podobnej składni, aby wymusić, że obiekt ma klucz dla każdej pozycji w typie unii:

type DayOfTheWeek = "sunday" | "monday" | "tuesday" | "wednesday" | "thursday" | "friday" | "saturday";

type ChoresMap = { [day in DayOfTheWeek]: string };

const chores: ChoresMap = { // ERROR! Property 'saturday' is missing in type '...'
    "sunday": "do the dishes",
    "monday": "walk the dog",
    "tuesday": "water the plants",
    "wednesday": "take out the trash",
    "thursday": "clean your room",
    "friday": "mow the lawn",
};

Możesz oczywiście uczynić to także typem ogólnym!

type DayOfTheWeek = "sunday" | "monday" | "tuesday" | "wednesday" | "thursday" | "friday" | "saturday";

type DayOfTheWeekMap<T> = { [day in DayOfTheWeek]: T };

const chores: DayOfTheWeekMap<string> = {
    "sunday": "do the dishes",
    "monday": "walk the dog",
    "tuesday": "water the plants",
    "wednesday": "take out the trash",
    "thursday": "clean your room",
    "friday": "mow the lawn",
    "saturday": "relax",
};

const workDays: DayOfTheWeekMap<boolean> = {
    "sunday": false,
    "monday": true,
    "tuesday": true,
    "wednesday": true,
    "thursday": true,
    "friday": true,
    "saturday": false,
};

Aktualizacja 10.10.2018: Sprawdź odpowiedź @ dracstaxi poniżej - teraz jest wbudowany typ, Recordktóry robi to za Ciebie.

Aktualizacja 1.2.2020: Całkowicie usunąłem gotowe interfejsy mapowania z mojej odpowiedzi. Odpowiedź @ dracstaxi czyni je całkowicie nieistotnymi. Jeśli nadal chcesz ich używać, sprawdź historię edycji.

Sandy Gifford
źródło
1
{[klucz: liczba]: T; } nie jest bezpieczny dla typów, ponieważ wewnętrznie klucze obiektu są zawsze ciągiem - więcej szczegółów można znaleźć w komentarzu do pytania @tep. np. Uruchomienie x = {}; x[1] = 2;w Chrome Object.keys(x)zwraca następnie [„1”] i JSON.stringify(x)zwraca „{„ 1 ”: 2}”. NumberSkrzynie narożne z typof (np. Infinity, NaN, 1e300, 99999999999999999999999 itp.) Są konwertowane na klucze łańcuchowe. Uważaj też na pozostałych przypadkach narożnych dla kluczy strunowych, takich jak x[''] = 'empty string';, x['000'] = 'threezeros'; x[undefined] = 'foo'.
robocat
@robocat To prawda, i edytowałem, żeby usunąć interfejsy z kluczem numerycznym z tej odpowiedzi. Ostatecznie zdecydowałem się je zachować, ponieważ TypeScript technicznie i konkretnie dopuszcza cyfry jako klucze. Powiedziawszy to, mam nadzieję, że każdy, kto zdecyduje się na użycie obiektów indeksowanych liczbami, zobaczy Twój komentarz.
Sandy Gifford
75

Szybka aktualizacja: od Typescript 2.1 istnieje wbudowany typ, Record<T, K>który działa jak słownik.

Przykład z dokumentów:

// For every properties K of type T, transform it to U
function mapObject<K extends string, T, U>(obj: Record<K, T>, f: (x: T) => U): Record<K, U>

const names = { foo: "hello", bar: "world", baz: "bye" };
const lengths = mapObject(names, s => s.length);  // { foo: number, bar: number, baz: number }

Dokumentacja TypeScript 2.1 włączona Record<T, K>

Jedyną wadą widzę użyciem nad tym {[key: T]: K}jest to, że można zakodować przydatnych informacji na temat, jaki rodzaj klucza używasz zamiast „klucza” np jeśli obiekt miał tylko klawisze prime można wskazywać na to tak: {[prime: number]: yourType}.

Oto regex, który napisałem, aby pomóc w tych konwersjach. Spowoduje to konwersję tylko przypadków, w których etykieta jest „kluczowa”. Aby przekonwertować inne etykiety, wystarczy zmienić pierwszą grupę przechwytywania:

Odnaleźć: \{\s*\[(key)\s*(+\s*:\s*(\w+)\s*\]\s*:\s*([^\}]+?)\s*;?\s*\}

Zastąpić: Record<$2, $3>

dracstaxi
źródło
3
To powinno uzyskać więcej głosów pozytywnych - jest to w zasadzie nowsza, natywna wersja mojej odpowiedzi.
Sandy Gifford,
Czy rekord można skompilować w plik {}?
Douglas Gaskell
Typy @DouglasGaskell nie są obecne w skompilowanym kodzie. Records (w przeciwieństwie do, powiedzmy, Javascript Map), umożliwiają jedynie wymuszenie zawartości literału obiektu. Nie możesz new Record...i const blah: Record<string, string>;będziesz się kompilować const blah;.
Sandy Gifford,
Nie możesz sobie nawet wyobrazić, jak bardzo ta odpowiedź mi pomogła, dziękuję bardzo :)
Federico Grandi
Warto wspomnieć, że związki strunowe działają Recordrównież w: const isBroken: Record<"hat" | "teapot" | "cup", boolean> = { hat: false, cup: false, teapot: true };
Sandy Gifford,
10

Odpowiedź Ryana Cavanaugha jest całkowicie poprawna i nadal aktualna. Warto jednak dodać, że od jesieni'16, kiedy możemy twierdzić, że ES6 jest obsługiwany przez większość platform, prawie zawsze lepiej jest trzymać się mapy, gdy potrzebujesz powiązać niektóre dane z jakimś kluczem.

Kiedy piszemy let a: { [s: string]: string; }, musimy pamiętać, że po skompilowaniu maszynopisu nie ma czegoś takiego jak typ danych, jest on używany tylko do kompilacji. I {[s: string]: string; } skompiluje się do {}.

To powiedziawszy, nawet jeśli napiszesz coś takiego:

class TrickyKey  {}

let dict: {[key:TrickyKey]: string} = {}

To po prostu się nie skompiluje (nawet dla target es6, dostanieszerror TS1023: An index signature parameter type must be 'string' or 'number'.

Tak więc praktycznie jesteś ograniczony ciągiem lub liczbą jako potencjalnym kluczem, więc nie ma tutaj większego sensu wymuszania sprawdzania typu, szczególnie biorąc pod uwagę, że gdy js próbuje uzyskać dostęp do klucza po numerze, konwertuje go na ciąg.

Można więc bezpiecznie założyć, że najlepszą praktyką jest używanie mapy, nawet jeśli klucze są ciągami, więc trzymałbym się:

let staff: Map<string, string> = new Map();
shabunc
źródło
4
Nie jestem pewien, czy było to możliwe, kiedy ta odpowiedź została opublikowana, ale dziś możesz to zrobić: let dict: {[key in TrickyKey]: string} = {}- gdzie TrickyKeyjest literał ciąg typu (np "Foo" | "Bar".).
Roman Starkov
Osobiście podoba mi się natywny format maszynopisu, ale masz rację najlepiej użyć standardu. Daje mi to sposób na udokumentowanie klucza „nazwa”, który tak naprawdę nie jest użyteczny, ale mogę zrobić klucz o nazwie „pacjent” :)
kodowanie
Ta odpowiedź jest absolutnie poprawna i zawiera bardzo dobre argumenty, ale nie zgadzam się z pomysłem, że prawie zawsze lepiej jest trzymać się rodzimych Mapobiektów. Mapy mają dodatkowy narzut pamięci i (co ważniejsze) muszą być tworzone ręcznie z dowolnych danych przechowywanych jako ciąg JSON. Często są bardzo przydatne, ale nie tylko w celu wydobycia z nich typów.
Sandy Gifford
7

Zdefiniuj interfejs

interface Settings {
  lang: 'en' | 'da';
  welcome: boolean;
}

Wymuś klucz, aby był określonym kluczem interfejsu ustawień

private setSettings(key: keyof Settings, value: any) {
   // Update settings key
}
Lasithds
źródło
3

Opierając się na odpowiedzi @ shabunc, pozwoliłoby to na wymuszenie klucza lub wartości - lub obu - na wszystko, co chcesz wymusić.

type IdentifierKeys = 'my.valid.key.1' | 'my.valid.key.2';
type IdentifierValues = 'my.valid.value.1' | 'my.valid.value.2';

let stuff = new Map<IdentifierKeys, IdentifierValues>();

Powinien również działać przy użyciu enumzamiast typedefinicji.

Roy Art
źródło