J'ai défini mon type générique comme
interface IDictionary<TValue> {
[key: string|number]: TValue;
}
Mais TSLint se plaint. Comment suis-je censé définir un type d'index d'objet pouvant avoir comme clé? J'ai essayé aussi mais pas de chance.
interface IDictionary<TKey, TValue> {
[key: TKey]: TValue;
}
interface IDictionary<TKey extends string|number, TValue> {
[key: TKey]: TValue;
}
type IndexKey = string | number;
interface IDictionary<TValue> {
[key: IndexKey]: TValue;
}
interface IDictionary<TKey extends IndexKey, TValue> {
[key: TKey]: TValue;
}
Aucun des travaux ci-dessus.
Vous pouvez y parvenir simplement en utilisant un IDictionary<TValue> { [key: string]: TValue }
puisque les valeurs numériques seront automatiquement converties en chaîne.
Voici un exemple d'utilisation:
interface IDictionary<TValue> {
[id: string]: TValue;
}
class Test {
private dictionary: IDictionary<string>;
constructor() {
this.dictionary = {}
this.dictionary[9] = "numeric-index";
this.dictionary["10"] = "string-index"
console.log(this.dictionary["9"], this.dictionary[10]);
}
}
// result => "numeric-index string-index"
Comme vous pouvez le constater, les index de chaîne et numériques sont interchangeables.
En javascript, les clés d'objet ne peuvent être que des chaînes de caractères (ainsi que des symboles es6
).
Si vous passez un nombre, il est converti en chaîne:
let o = {};
o[3] = "three";
console.log(Object.keys(o)); // ["3"]
Comme vous pouvez le constater, vous obtenez toujours { [key: string]: TValue; }
.
TypeScript vous permet de définir une carte comme ceci avec number
s comme clés:
type Dict = { [key: number]: string };
Et le compilateur vérifiera que lors de l'affectation de valeurs, vous passez toujours un nombre en tant que clé, mais qu'au moment de l'exécution, les clés de l'objet seront des chaînes.
Vous pouvez donc avoir soit { [key: number]: string }
ou { [key: string]: string }
, mais pas une union de string | number
, pour les raisons suivantes:
let d = {} as IDictionary<string>;
d[3] = "1st three";
d["3"] = "2nd three";
Vous pourriez vous attendre à ce que d
ait deux entrées différentes ici, mais en fait il n'y en a qu'une.
Ce que vous pouvez faire, c'est utiliser une Map
:
let m = new Map<number|string, string>();
m.set(3, "1st three");
m.set("3", "2nd three");
Ici, vous aurez deux entrées différentes.
Bien que les clés d'objet soient toujours des chaînes sous le capot et que vous utilisiez des indexeurs comme des chaînes pour couvrir des nombres, vous souhaitez parfois qu'une fonction prenne conscience des clés des objets qui y sont passés. Considérons cette fonction de mappage qui fonctionne comme Array.map
mais avec des objets:
function map<T>(obj: Object, callback: (key: string, value: any) => T): T[] {
// ...
}
key
est limité à string
et la valeur est entièrement non typée. Probablement bien 9 fois sur 10, mais nous pouvons faire mieux. Disons que nous voulions faire quelque chose d'idiot comme celui-ci:
const obj: {[key: number]: string} = { 1: "hello", 2: "world", 3: "foo", 4: "bar" };
map(obj, (key, value) => `${key / 2} ${value}`);
// error: The left-hand side of an arithmetic operation must be of type 'any', 'number' or an enum type.
Nous ne pouvons effectuer aucune opération arithmétique sur une clé sans l'avoir d'abord numérotée (rappelez-vous: "3" / 2
est valide dans JS et se transforme en number
). Nous pouvons résoudre ce problème en tapant un peu sur notre fonction de carte:
function map<S, T>(obj: S, callback: (key: keyof S, value: S[keyof S]) => T): T[] {
return Object.keys(obj).map(key => callback(key as any, (obj as any)[key]));
}
Ici, nous utilisons la variable S
générique pour taper notre objet, et recherchons les types de clé et de valeur directement à partir de celui-ci. Si votre objet est typé à l'aide d'indexeurs et de valeurs génériques, keyof S
et S[keyof S]
seront résolus en types constants. Si vous transmettez un objet avec des propriétés explicites, keyof S
sera limité aux noms de propriété et S[keyof S]
sera limité aux types de valeur de propriété.