Den Typ der indizierten Elemente eines Typescript-Objekts erzwingen?


289

Ich möchte eine Zuordnung von Zeichenfolge -> Zeichenfolge in einem Typescript-Objekt speichern und erzwingen, dass alle Schlüssel Zeichenfolgen zugeordnet werden. Beispielsweise:

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

Gibt es eine Möglichkeit für mich, zu erzwingen, dass die Werte Zeichenfolgen sein müssen (oder welcher Typ auch immer ..)?

Antworten:


518
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

13
Das Interessante an dieser Syntax ist, dass jeder andere Typ als 'string' für den Schlüssel tatsächlich falsch ist. Dies ist durchaus sinnvoll, da JS-Maps explizit durch Zeichenfolgen verschlüsselt sind, die Syntax jedoch etwas überflüssig wird. Etwas wie '{}: string', um einfach die Typen der Werte anzugeben, scheint einfacher zu sein, es sei denn, sie werden auf irgendeine Weise hinzugefügt, um das automatische Erzwingen der Schlüssel als Teil des generierten Codes zu ermöglichen.
Armentage

42
numberist auch als Indizierungstyp erlaubt
Ryan Cavanaugh

3
Bemerkenswert: Der Compiler erzwingt nur den Werttyp, nicht den Schlüsseltyp. du kannst Sachen machen [15] = 'was auch immer' und es wird sich nicht beschweren.
Amwinter

3
Nein, der Schlüsseltyp wird erzwungen. Sie können nichts tun [myObject] = 'was auch immer', selbst wenn myObject eine nette toString () -Implementierung hat.
AlexG

7
@RyanCavanaugh Ist es möglich (oder wird es sein), einen type Keys = 'Acceptable' | 'String' | 'Keys'als Indizierungs- (Schlüssel-) Typ zu verwenden?
Calebboyd

130
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'.
};

Hier AgeMaperzwingt die Schnittstelle Schlüssel als Zeichenfolgen und Werte als Zahlen. Das Schlüsselwort namekann eine beliebige Kennung sein und sollte verwendet werden, um die Syntax Ihrer Schnittstelle / Ihres Typs vorzuschlagen.

Sie können eine ähnliche Syntax verwenden, um zu erzwingen, dass ein Objekt für jeden Eintrag in einem Unionstyp einen Schlüssel hat:

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",
};

Sie können dies natürlich auch zu einem generischen Typ machen!

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,
};

10.10.2018 Update: Schauen Sie sich die Antwort von @ dracstaxi unten an - es gibt jetzt einen integrierten Typ, Recordder das meiste für Sie erledigt.

1.2.2020 Update: Ich habe die vorgefertigten Mapping-Schnittstellen vollständig aus meiner Antwort entfernt. @ dracstaxis Antwort macht sie völlig irrelevant. Wenn Sie sie weiterhin verwenden möchten, überprüfen Sie den Bearbeitungsverlauf.


1
{[Schlüssel: Nummer]: T; } ist nicht typsicher, da die Schlüssel eines Objekts intern immer eine Zeichenfolge sind. Weitere Informationen finden Sie im Kommentar zur Frage von @tep. Wenn Sie beispielsweise x = {}; x[1] = 2;in Chrome ausgeführt werden, wird Object.keys(x)["1"] und JSON.stringify(x)"{" 1 ": 2}" zurückgegeben. Eckfällen mit TypeOf Number(zB Unendlichkeit, NaN, 1e300, 999999999999999999999 usw.) erhalten , um String - Schlüssel umgewandelt. Achten Sie auch auf andere Eckfälle für String-Schlüssel wie x[''] = 'empty string';, x['000'] = 'threezeros'; x[undefined] = 'foo'.
Robocat

@robocat Dies ist wahr, und ich bin beim Bearbeiten hin und her gegangen, um die nummerierten Schnittstellen für eine Weile aus dieser Antwort zu entfernen. Letztendlich habe ich beschlossen, sie beizubehalten, da TypeScript technisch und spezifisch Zahlen als Schlüssel zulässt. Trotzdem hoffe ich, dass jeder, der sich für die Verwendung von mit Zahlen indizierten Objekten entscheidet, Ihren Kommentar sieht.
Sandy Gifford

75

Ein schnelles Update: Seit Typescript 2.1 gibt es einen eingebauten Typ Record<T, K>, der sich wie ein Wörterbuch verhält.

Ein Beispiel aus Dokumenten:

// 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 }

TypeScript 2.1 Dokumentation ein Record<T, K>

Der einzige Nachteil, den ich bei der Verwendung sehe, {[key: T]: K}ist, dass Sie nützliche Informationen darüber codieren können, welche Art von Schlüssel Sie anstelle von "Schlüssel" verwenden, z. B. wenn Ihr Objekt nur Primschlüssel hatte, können Sie dies wie folgt andeuten : {[prime: number]: yourType}.

Hier ist ein regulärer Ausdruck, den ich geschrieben habe, um bei diesen Konvertierungen zu helfen. Dies konvertiert nur Fälle, in denen die Bezeichnung "Schlüssel" ist. Um andere Beschriftungen zu konvertieren, ändern Sie einfach die erste Erfassungsgruppe:

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

Ersetzen: Record<$2, $3>


3
Dies sollte mehr positive Stimmen bekommen - es ist im Wesentlichen die neuere, native Version meiner Antwort.
Sandy Gifford

Kompiliert record in a {}?
Douglas Gaskell

@ DouglasGaskell-Typen sind im kompilierten Code nicht vorhanden. Records (im Gegensatz zu beispielsweise Javascript Maps) bieten nur eine Möglichkeit, den Inhalt eines Objektliteral durchzusetzen. Sie können new Record...und const blah: Record<string, string>;werden nicht kompilieren const blah;.
Sandy Gifford

Sie können sich nicht einmal vorstellen, wie sehr mir diese Antwort geholfen hat, vielen Dank :)
Federico Grandi

Erwähnenswert ist, dass String-Gewerkschaften auch in Records funktionieren : const isBroken: Record<"hat" | "teapot" | "cup", boolean> = { hat: false, cup: false, teapot: true };
Sandy Gifford

10

@ Ryan Cavanaughs Antwort ist völlig in Ordnung und immer noch gültig. Es lohnt sich dennoch hinzuzufügen, dass es ab Herbst 16, wenn wir behaupten können, dass ES6 von den meisten Plattformen unterstützt wird, fast immer besser ist, sich an Map zu halten, wenn Sie Daten mit einem Schlüssel verknüpfen müssen.

Wenn wir schreiben let a: { [s: string]: string; }, müssen wir uns daran erinnern, dass es nach dem Kompilieren von Typoskripten keine Typdaten gibt, sondern nur zum Kompilieren. Und {[s: string]: string; } wird nur zu {} kompiliert.

Das heißt, auch wenn Sie etwas schreiben wie:

class TrickyKey  {}

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

Dies wird einfach nicht kompiliert (auch für target es6, Sie werden bekommenerror TS1023: An index signature parameter type must be 'string' or 'number'.

Praktisch sind Sie also auf eine Zeichenfolge oder eine Nummer als potenziellen Schlüssel beschränkt, sodass es nicht so sinnvoll ist, die Typprüfung zu erzwingen, insbesondere wenn js versucht, auf den Schlüssel anhand der Nummer zuzugreifen, wird diese in eine Zeichenfolge konvertiert.

Es ist also ziemlich sicher anzunehmen, dass die beste Vorgehensweise darin besteht, Map zu verwenden, auch wenn die Schlüssel Zeichenfolgen sind. Ich würde mich also an Folgendes halten:

let staff: Map<string, string> = new Map();

4
Ich bin mir nicht sicher, ob dies möglich war, als diese Antwort veröffentlicht wurde, aber heute können Sie Folgendes tun: let dict: {[key in TrickyKey]: string} = {}- Wo TrickyKeyist ein String-Literal-Typ (z "Foo" | "Bar". B. )?
Roman Starkov

Persönlich mag ich das native Typoskriptformat, aber Sie haben Recht, es ist am besten, den Standard zu verwenden. Es gibt mir eine Möglichkeit, den Schlüssel "Name" zu dokumentieren, der nicht wirklich verwendbar ist, aber ich kann den Schlüssel so etwas wie "patientId" machen :)
Codierung

Diese Antwort ist absolut gültig und macht sehr gute Punkte, aber ich würde der Idee nicht zustimmen, dass es fast immer besser ist, sich an native MapObjekte zu halten. Maps sind mit zusätzlichem Speicheraufwand verbunden und müssen (was noch wichtiger ist) manuell aus allen Daten instanziiert werden, die als JSON-Zeichenfolge gespeichert sind. Sie sind oft sehr nützlich, aber nicht nur, um Typen aus ihnen herauszuholen.
Sandy Gifford

7

Schnittstelle definieren

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

Erzwingen Sie, dass der Schlüssel ein bestimmter Schlüssel der Einstellungsoberfläche ist

private setSettings(key: keyof Settings, value: any) {
   // Update settings key
}

3

Aufbauend auf der Antwort von @ shabunc würde dies ermöglichen, dass entweder der Schlüssel oder der Wert - oder beides - alles ist, was Sie erzwingen möchten.

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>();

Sollte auch mit enumanstelle einer typeDefinition funktionieren .

Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.