Applicare il tipo di membri indicizzati di un oggetto Typescript?


291

Vorrei archiviare una mappatura di stringa -> stringa in un oggetto Typescript e imporre che tutte le chiavi siano mappate su stringhe. Per esempio:

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

Esiste un modo per imporre che i valori debbano essere stringhe (o qualunque tipo ...)?

Risposte:


523
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
La cosa interessante di questa sintassi è che qualsiasi tipo diverso da "stringa" per la chiave è effettivamente sbagliato. Ha perfettamente senso, dato che le mappe JS sono esplicitamente codificate da stringhe, ma rende la sintassi in qualche modo ridondante. Qualcosa come '{}: string' per specificare semplicemente i tipi di valori sembrerebbe più semplice, a meno che non vengano aggiunti in qualche modo per consentire la coercizione automatica delle chiavi come parte del codice generato.
Armentage,

43
numberè anche consentito come tipo di indicizzazione
Ryan Cavanaugh,

3
vale la pena notare: il compilatore applica solo il tipo di valore, non il tipo di chiave. puoi fare cose [15] = 'qualunque cosa' e non si lamenterà.
amwinter

3
No, applica il tipo di chiave. Non puoi fare cose [myObject] = 'qualunque' 'anche se myObject ha una bella implementazione toString ().
AlexG,

7
@RyanCavanaugh È possibile (o sarà) utilizzare un type Keys = 'Acceptable' | 'String' | 'Keys'tipo di indicizzazione (chiave)?
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'.
};

Qui, l'interfaccia AgeMapimpone chiavi come stringhe e valori come numeri. La parola chiave namepuò essere qualsiasi identificatore e deve essere utilizzata per suggerire la sintassi della tua interfaccia / tipo.

È possibile utilizzare una sintassi simile per imporre che un oggetto abbia una chiave per ogni voce in un tipo di unione:

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

Naturalmente puoi anche rendere questo un tipo generico!

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

Aggiornamento del 10.10.2018: dai un'occhiata alla risposta di @ dracstaxi qui sotto: ora c'è un tipo integrato Recordche fa la maggior parte di questo per te.

Aggiornamento 1.2.2020: ho completamente rimosso le interfacce di mappatura pre-create dalla mia risposta. La risposta di @ dracstaxi li rende totalmente irrilevanti. Se desideri comunque usarli, controlla la cronologia delle modifiche.


1
{[chiave: numero]: T; } non è un errore di battitura perché internamente le chiavi di un oggetto sono sempre una stringa - vedi il commento sulla domanda di @tep per maggiori dettagli. ad esempio, l'esecuzione x = {}; x[1] = 2;in Chrome Object.keys(x)restituisce ["1"] e JSON.stringify(x)restituisce "{" 1 ": 2}". Le custodie angolari con typeof Number(ad es. Infinity, NaN, 1e300, 99999999999999999999999 ecc.) Vengono convertite in chiavi stringa. Anche attenzione di altri casi d'angolo per chiavi stringa come x[''] = 'empty string';, x['000'] = 'threezeros'; x[undefined] = 'foo'.
robocat,

@robocat Questo è vero, e sono andato avanti e indietro sulla modifica per rimuovere il numero di interfacce con chiave da questa risposta per un po '. Alla fine ho deciso di mantenerli dal momento che TypeScript tecnicamente e specificamente consente numeri come chiavi. Detto questo, spero che chiunque decida di usare oggetti indicizzati con numeri veda il tuo commento.
Sandy Gifford,

76

Un rapido aggiornamento: da Typescript 2.1 esiste un tipo incorporato Record<T, K>che si comporta come un dizionario.

Un esempio dai documenti:

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

Documentazione di TypeScript 2.1 su Record<T, K>

L'unico svantaggio che vedo di utilizzare questo corso {[key: T]: K}è che è possibile codificare informazioni utili su che tipo di chiave che si sta utilizzando al posto di "chiave" per esempio se l'oggetto aveva solo le chiavi principali si potrebbe accennare a quel modo: {[prime: number]: yourType}.

Ecco una regex che ho scritto per aiutare con queste conversioni. Questo convertirà solo i casi in cui l'etichetta è "chiave". Per convertire altre etichette basta cambiare il primo gruppo di acquisizione:

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

Sostituire: Record<$2, $3>


3
Questo dovrebbe ottenere più voti: è essenzialmente la versione nativa più recente della mia risposta.
Sandy Gifford,

Il record viene compilato in un {}?
Douglas Gaskell,

I tipi @DouglasGaskell non hanno alcuna presenza nel codice compilato. Records (a differenza di, per esempio, Javascript Map) fornisce solo un modo per imporre il contenuto di un oggetto letterale. Non puoi new Record...e const blah: Record<string, string>;lo compilerai const blah;.
Sandy Gifford,

Non puoi nemmeno immaginare quanto questa risposta mi abbia aiutato, grazie mille :)
Federico Grandi,

Vale la pena ricordare che i sindacati delle stringhe funzionano anche in Records: const isBroken: Record<"hat" | "teapot" | "cup", boolean> = { hat: false, cup: false, teapot: true };
Sandy Gifford il

10

La risposta di @Ryan Cavanaugh è totalmente ok e comunque valida. Vale comunque la pena aggiungere che a partire dall'autunno 16, quando possiamo affermare che ES6 è supportato dalla maggior parte delle piattaforme, è quasi sempre meglio attenersi a Map ogni volta che è necessario associare alcuni dati a una chiave.

Quando scriviamo, let a: { [s: string]: string; }dobbiamo ricordare che dopo la compilazione di dattiloscritto non esistono dati di tipo, ma vengono utilizzati solo per la compilazione. E {[s: string]: string; } verrà compilato in solo {}.

Detto questo, anche se scriverai qualcosa del tipo:

class TrickyKey  {}

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

Questo non si compila (anche per target es6, otterraierror TS1023: An index signature parameter type must be 'string' or 'number'.

Quindi praticamente sei limitato con stringa o numero come chiave potenziale, quindi non c'è molto senso di forzare il controllo del tipo qui, soprattutto tenendo presente che quando js tenta di accedere a chiave per numero, la converte in stringa.

Quindi è abbastanza sicuro supporre che la migliore pratica sia usare Map anche se le chiavi sono stringhe, quindi dovrei attenermi a:

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

4
Non sono sicuro che questo fosse possibile quando è stata pubblicata questa risposta, ma oggi puoi farlo: let dict: {[key in TrickyKey]: string} = {}- dov'è TrickyKeyun tipo letterale di stringa (ad es "Foo" | "Bar".).
Roman Starkov,

Personalmente mi piace il formato dattiloscritto nativo, ma hai ragione di usare lo standard. Mi dà un modo per documentare la chiave "nome" che non è realmente utilizzabile ma posso fare la chiave chiamata qualcosa come "patientId" :)
codifica

Questa risposta è assolutamente valida e fa ottimi punti, ma non sono d'accordo con l'idea che sia quasi sempre meglio attenersi agli Mapoggetti nativi . Le mappe hanno un sovraccarico di memoria aggiuntivo e (cosa ancora più importante) devono essere istanziate manualmente da tutti i dati archiviati come stringa JSON. Sono spesso molto utili, ma non puramente per il gusto di tirarne fuori i tipi.
Sandy Gifford,

7

Definire l'interfaccia

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

Imponi chiave come chiave specifica dell'interfaccia Impostazioni

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

3

Basandosi sulla risposta di @ shabunc, ciò consentirebbe di applicare la chiave o il valore - o entrambi - per essere tutto ciò che si desidera applicare.

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

Dovrebbe funzionare anche usando enuminvece di una typedefinizione.

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.