Appliquer le type des membres indexés d'un objet Typescript?

292

Je voudrais stocker un mappage de chaîne -> chaîne dans un objet Typescript, et imposer que toutes les clés mappent sur des chaînes. Par exemple:

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

Existe-t-il un moyen pour moi d'imposer que les valeurs doivent être des chaînes (ou tout autre type ..)?

Armentage
la source

Réponses:

524
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
la source
13
Ce qui est intéressant à propos de cette syntaxe, c'est que tout type autre que «chaîne» pour la clé est en fait faux. Est parfaitement logique, étant donné que les mappages JS sont explicitement incrustés par des chaînes, mais cela rend la syntaxe quelque peu redondante. Quelque chose comme '{}: string' pour simplement spécifier les types de valeurs semblerait plus simple, à moins qu'ils ne s'ajoutent d'une manière ou d'une autre pour permettre la coercition automatique des clés dans le cadre du code généré.
Armentage du
43
numberest également autorisé comme type d'indexation
Ryan Cavanaugh
3
à noter: le compilateur applique uniquement le type de valeur, pas le type de clé. vous pouvez faire des choses [15] = 'n'importe quoi' et ça ne vous plaindra pas.
amwinter
3
Non, il applique le type de clé. Vous ne pouvez pas faire des choses [myObject] = 'n'importe quoi' même si myObject a une belle implémentation de toString ().
AlexG
7
@RyanCavanaugh Est-il possible (ou le sera) d'utiliser un type Keys = 'Acceptable' | 'String' | 'Keys'type d'indexation (clé)?
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'.
};

Ici, l'interface AgeMapapplique les clés sous forme de chaînes et les valeurs sous forme de nombres. Le mot-clé namepeut être n'importe quel identifiant et doit être utilisé pour suggérer la syntaxe de votre interface / type.

Vous pouvez utiliser une syntaxe similaire pour imposer qu'un objet possède une clé pour chaque entrée d'un type d'union:

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

Vous pouvez bien sûr en faire également un type générique!

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

Mise à jour du 10.10.2018: Découvrez la réponse de @ dracstaxi ci-dessous - il y a maintenant un type intégré Recordqui fait la plupart de cela pour vous.

Mise à jour 1.2.2020: j'ai entièrement supprimé les interfaces de mappage prédéfinies de ma réponse. La réponse de @ dracstaxi les rend totalement hors de propos. Si vous souhaitez toujours les utiliser, consultez l'historique des modifications.

Sandy Gifford
la source
1
{[clé: numéro]: T; } n'est pas de type sécurisé car en interne les clés d'un objet sont toujours une chaîne - voir le commentaire sur la question par @tep pour plus de détails. par exemple en cours d'exécution x = {}; x[1] = 2;dans Chrome puisObject.keys(x) renvoie ["1"] et JSON.stringify(x)renvoie '{"1": 2}'. Les cas d'angle avec typeof Number(par exemple Infinity, NaN, 1e300, 999999999999999999999 etc.) sont convertis en clés de chaîne. Méfiez - vous également d'autres cas de coin pour les clés de chaîne comme x[''] = 'empty string';, x['000'] = 'threezeros'; x[undefined] = 'foo'.
robocat
@robocat C'est vrai, et j'ai fait des allers-retours sur l'édition pour supprimer le nombre d'interfaces à clé de cette réponse pendant un certain temps. En fin de compte, j'ai décidé de les conserver car TypeScript autorise techniquement et spécifiquement les nombres sous forme de clés. Cela dit, j'espère que quiconque décide d'utiliser des objets indexés avec des chiffres voit votre commentaire.
Sandy Gifford
76

Une mise à jour rapide: depuis Typescript 2.1, il existe un type intégré Record<T, K>qui agit comme un dictionnaire.

Un exemple de documents:

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

Documentation TypeScript 2.1 sur Record<T, K>

Le seul inconvénient que je vois à utiliser ceci {[key: T]: K}est que vous pouvez encoder des informations utiles sur le type de clé que vous utilisez à la place de "clé" {[prime: number]: yourType}.

Voici une expression régulière que j'ai écrite pour aider à ces conversions. Cela ne convertira que les cas où l'étiquette est "clé". Pour convertir d'autres étiquettes, changez simplement le premier groupe de capture:

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

Remplacer: Record<$2, $3>

dracstaxi
la source
3
Cela devrait obtenir plus de votes positifs - c'est essentiellement la version native la plus récente de ma réponse.
Sandy Gifford
L'enregistrement se compile-t-il en un {}?
Douglas Gaskell
Les types @DouglasGaskell n'ont aucune présence dans le code compilé. Records (contrairement à, disons, Javascript Map) ne fournissent qu'un moyen d'appliquer le contenu d'un objet littéral. Vous ne pouvez pas new Record...et const blah: Record<string, string>;allez compiler const blah;.
Sandy Gifford
Vous ne pouvez même pas imaginer à quel point cette réponse m'a aidé, merci beaucoup :)
Federico Grandi
Il convient de mentionner que les syndicats de cordes fonctionnent également dans Record: const isBroken: Record<"hat" | "teapot" | "cup", boolean> = { hat: false, cup: false, teapot: true };
Sandy Gifford
10

La réponse de @Ryan Cavanaugh est totalement correcte et toujours valable. Il vaut toujours la peine d'ajouter qu'à partir de l'automne 2016, lorsque nous pouvons affirmer que ES6 est pris en charge par la majorité des plates-formes, il est presque toujours préférable de s'en tenir à Map chaque fois que vous avez besoin d'associer certaines données à une clé.

Quand on écrit let a: { [s: string]: string; } nous devons nous rappeler qu'après la compilation du typage, il n'y a pas de données de type, elles ne sont utilisées que pour la compilation. Et {[s: chaîne]: chaîne; } sera compilé en {} seulement.

Cela dit, même si vous écrivez quelque chose comme:

class TrickyKey  {}

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

Cela ne compilera tout simplement pas (même pour target es6 , vous obtiendrezerror TS1023: An index signature parameter type must be 'string' or 'number'.

Donc, pratiquement, vous êtes limité avec une chaîne ou un nombre comme clé potentielle, il n'y a donc pas beaucoup de sens à appliquer la vérification de type ici, surtout en gardant à l'esprit que lorsque js essaie d'accéder à la clé par numéro, il la convertit en chaîne.

Il est donc tout à fait sûr de supposer que la meilleure pratique consiste à utiliser Map même si les clés sont des chaînes, alors je m'en tiendrai à:

let staff: Map<string, string> = new Map();
shabunc
la source
4
Je ne sais pas si cela était possible lorsque cette réponse a été publiée, mais aujourd'hui, vous pouvez le faire: let dict: {[key in TrickyKey]: string} = {}- où TrickyKeyest un type littéral de chaîne (par exemple "Foo" | "Bar").
Roman Starkov
Personnellement, j'aime le format natif dactylographié, mais vous avez raison d'utiliser le standard. Cela me donne un moyen de documenter la clé "nom" qui n'est pas vraiment utilisable mais je peux faire la clé appelée quelque chose comme "patientId" :)
codage le
Cette réponse est absolument valable et fait de très bons points, mais je ne suis pas d'accord avec l'idée qu'il est presque toujours préférable de s'en tenir aux Mapobjets natifs . Les cartes sont livrées avec une surcharge de mémoire supplémentaire et (plus important encore) doivent être instanciées manuellement à partir de toutes les données stockées sous forme de chaîne JSON. Ils sont souvent très utiles, mais pas uniquement pour en retirer des types.
Sandy Gifford
7

Définir l'interface

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

Appliquer la clé comme clé spécifique de l'interface des paramètres

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

En s'appuyant sur la réponse de @ shabunc, cela permettrait d'appliquer la clé ou la valeur - ou les deux - à tout ce que vous souhaitez appliquer.

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

Devrait également fonctionner en utilisant enumau lieu d'une typedéfinition.

Roy Art
la source