Apprenez à créer une fonction typescript qui convertit des tableaux en dictionnaires sécurisés au niveau des types et maîtrisez les types conditionnels avec l'inférence de types.
Avez-vous déjà eu besoin de convertir rapidement un tableau en dictionnaire de recherche? Bien que TypeScript fournisse les outils de base, vous vous retrouverez souvent à écrire du code de mappage répétitif. Voici l'approche standard:
interface Person { key: number, name: string, birthday: Date }
const data: Person[] = [
{ key: 1, name: 'Rupert', birthday: new Date('1986-04-10') },
{ key: 2, name: 'Anna', birthday: new Date('1989-10-10') },
];
// Créer une map en utilisant la Map prête à l'emploi et Array.map
const dict = new Map(data.map(x => [x.key, x]))
// ^ const dict: Map<number, Person>
Bien que cela fonctionne, vous remarquerez rapidement que cela semble verbeux et répétitif. Et si vous pouviez écrire du code plus intuitif qui soit à la fois flexible et sécurisé au niveau des types? La fonction toMap ci-dessous vous offre exactement cela - prenant en charge à la fois les noms de propriétés et les fonctions personnalisées:
// Créer une map en utilisant la propriété 'key'
const map1 = toMap(data, 'key');
// ^ const map1: Map<number, Person>
// Créer une map en utilisant la fonction 'key'
const map2 = toMap(data, x => x.key);
// ^ const map2: Map<number, Person>
// Créer une map en utilisant la propriété 'key' et la propriété 'value'
const map3 = toMap(data, 'key', 'name');
// ^ const map3: Map<number, string>
// Créer une map en utilisant la fonction 'key' et la fonction 'value'
const map4 = toMap(data, x => x.key, x => x.name);
// ^ const map4: Map<number, string>
// Créer une map en utilisant des fonctions personnalisées
const map5 = toMap(data, x => x.birthday, x => `Name: ${x.name}`);
// ^ const map5: Map<Date, string>
// Créer une map sans paramètres
const map6 = toMap(data);
// ^ const map6: Map<Person, Person>
Prêt à construire cet utilitaire puissant? Suivez-moi pendant que nous le construisons étape par étape. Chaque itération vous enseignera de nouveaux concepts TypeScript que vous pourrez appliquer à vos propres projets: (cliquez sur les bords du bloc de code pour avancer ou reculer dans les versions)
// Commencez simplement - vous construirez la complexité progressivement
export function toMap<T, K>(
array: readonly T[],
keyFn: ((item: T) => K),
) {
if (!array?.length) return new Map<K, T>();
return new Map<K, T>(array.map(item => [keyFn(item), item]));
}
const dict = toMap(data, x => x.birthday);
// ^ const dict: Map<Date, Person>
// Nous ajoutons de l'espace pour passer par les étapes sans que les lignes ne sautent
export function toMap<
T,
K
>(
array: readonly T[],
keyFn: ((item: T) => K),
) {
if (!array?.length) return new Map<K, T>();
return new Map<K, T>(array.map(item => [keyFn(item), item]));
}
const dict = toMap(data, x => x.birthday);
// ^ const dict: Map<Date, Person>
// Maintenant vous apprendrez les types conditionnels - développez le paramètre key pour accepter les noms de propriétés// et déplacez le type key vers les génériques pour pouvoir le référencer plus tardexport function toMap<
T,
K,
KF extends keyof T | ((item: T) => K) >(
array: readonly T[],
key: KF,
) {
// - Créez votre premier type conditionnel - il choisit entre le type de propriété ou le type de retour de fonction type TKeyType = KF extends keyof T ? T[KF] : KF extends ((item: T) => K) ? K : never;
if (!array?.length) return new Map<TKeyType, T>();
// - Vous rencontrerez une erreur TypeScript ici - Type 'K' is not assignable to type 'TKeyType' const keyFn: (item: T) => TKeyType = typeof key === 'function'
? key
: typeof array[0] === 'object' && array[0] !== null
? item => item[key as keyof T]
: item => item; // Fallback when the array contains primitive values
return new Map<TKeyType, T>(array.map(item => [keyFn(item), item]));
}
// - Pas encore la sécurité des types que vous voulezconst dict = toMap(data, x => x.birthday); // ^ const dict: Map<unknown, Person>// Voici comment résoudre l'erreur - utilisez l'inférence de type à la place// - Supprimez le paramètre de type K et laissez TypeScript l'inférerexport function toMap<
T,
KF extends keyof T | ((item: T) => any) >(
array: readonly T[],
key: KF,
) {
// - Laissez TypeScript inférer le type de retour pour vous type TKeyType = KF extends keyof T
? T[KF]
: KF extends ((item: T) => infer K) ? K
: never;
if (!array?.length) return new Map<TKeyType, T>();
const keyFn: (item: T) => TKeyType
= typeof key === 'function'
? key
: typeof array[0] === 'object' && array[0] !== null
? item => item[key as keyof T]
: item => item;
return new Map<TKeyType, T>(array.map(item => [keyFn(item), item]));
}
// - Parfait! Maintenant vous obtenez une inférence de type appropriéeconst dict = toMap(data, x => x.birthday); // ^ const dict: Map<Date, Person>// Rendez la fonction plus flexible - les paramètres optionnels vous permettent d'en faire plus
export function toMap<
T,
KF extends keyof T | ((item: T) => any) | undefined, >(
array: readonly T[],
key?: KF,
) {
// - Gérez le cas optionnel dans vos types conditionnels, en retournant T type TKeyType = undefined extends KF ? T : KF extends keyof T
? T[KF]
: KF extends ((item: T) => infer K)
? K
: never;
if (!array?.length)
return new Map<TKeyType, T>();
const keyFn: (item: T) => TKeyType
= typeof key === 'function'
? key
: key !== undefined && typeof array[0] === 'object' && array[0] !== null ? item => item[key as keyof T]
: item => item;
return new Map<TKeyType, T>(array.map(item => [keyFn(item), item]));
}
const dict = toMap(data);
// ^ const dict: Map<Person, Person>// Appliquez ce que vous avez appris - ajoutez le paramètre value en utilisant le même modèle
export function toMap<
T,
KF extends keyof T | ((item: T) => any) | undefined,
KV extends keyof T | ((item: T) => any) | undefined, >(
array: readonly T[],
key?: KF,
value?: KV, ) {
type TKeyType = undefined extends KF ? T : KF extends keyof T ? T[KF] : KF extends ((item: T) => infer K) ? K : never;
type TValueType = undefined extends KV ? T : KV extends ((item: T) => infer VF) ? VF : KV extends keyof T ? T[KV] : never;
if (!array?.length)
return new Map<TKeyType, TValueType>();
const keyFn: (item: T) => TKeyType
= typeof key === 'function'
? key
: key !== undefined && typeof array[0] === 'object' && array[0] !== null
? item => item[key as keyof T]
: item => item;
const valueFn: (item: T) => TValueType = typeof value === 'function' ? value : value !== undefined && typeof array[0] === 'object' && array[0] !== null ? item => item[value as keyof T] : item => item;
return new Map<TKeyType, TValueType>(array.map(item => [keyFn(item), valueFn(item)]));
}
const dict = toMap(data, 'key', 'birthday');
// ^ const dict: Map<number, Date>// Votre résultat final - un utilitaire puissant et sécurisé au niveau des types que vous pouvez utiliser partout
/**
* Aide à créer une map (dictionnaire) à partir d'une liste d'éléments.
* Pour les clés et les valeurs, vous pouvez fournir soit un keyof T soit une fonction qui retourne la valeur.
* Si vous ne spécifiez aucune valeur, l'élément entier sera utilisé comme valeur.
* @param array le tableau à convertir
* @param key un keyof d'un élément du tableau | une fonction qui retourne une valeur représentant la clé du dictionnaire
* @param value un keyof d'un élément du tableau | une fonction qui retourne n'importe quelle valeur
* @returns une nouvelle Map()
*/
export function toMap<
T,
KF extends keyof T | ((item: T) => any) | undefined,
KV extends keyof T | ((item: T) => any) | undefined,
>(
array: readonly T[],
key?: KF,
value?: KV,
) {
type TKeyType = undefined extends KF ? T : KF extends keyof T ? T[KF] : KF extends ((item: T) => infer K) ? K : never;
type TValueType = undefined extends KV ? T : KV extends ((item: T) => infer VF) ? VF : KV extends keyof T ? T[KV] : never;
if (!array?.length)
return new Map<TKeyType, TValueType>();
const keyFn: (item: T) => TKeyType
= typeof key === 'function'
? key
: key !== undefined && typeof array[0] === 'object' && array[0] !== null
? item => item[key as keyof T]
: item => item;
const valueFn: (item: T) => TValueType
= typeof value === 'function'
? value
: value !== undefined && typeof array[0] === 'object' && array[0] !== null
? item => item[value as keyof T]
: item => item;
// Créer la map et parcourir le tableau une seule fois
const map = new Map<TKeyType, TValueType>();
array.forEach((item) => {
map.set(keyFn(item), valueFn(item));
});
return map;
}
const dict = toMap(data, 'key', x => x.birthday);
// ^ const dict: Map<number, Date>
Vous voulez voir à quel point cette fonction est testée en profondeur? Consultez le fichier de test complet ici toMap.tests.ts.
Travailler avec strictNullChecks désactivé
Lorsque strictNullChecks est désactivé (ou que le mode strict lui-même est désactivé), undefined devient assignable à tous les types, ce qui casse notre type conditionnel.
Dans ce scénario, quels que soient les paramètres passés à toMap, la map résultante aura toujours les types de clé et de valeur T.
Pour gérer cette limitation, vous pouvez utiliser des surcharges de fonction pour fournir des signatures de type explicites qui correspondent aux différentes combinaisons de paramètres, garantissant la sécurité des types même lorsque les vérifications strictes de null sont désactivées.
Voici comment vous pouvez implémenter cela:
/**
* Aide à créer une map (dictionnaire) à partir d'une liste d'éléments.
* Pour les clés et les valeurs, vous pouvez fournir soit un keyof T soit une fonction qui retourne la valeur.
* Si vous ne spécifiez aucune valeur, l'élément entier sera utilisé comme valeur.
*
* @param array le tableau à convertir
* @param key un keyof d'un élément du tableau | une fonction qui retourne une valeur représentant la clé du dictionnaire
* @param value un keyof d'un élément du tableau | une fonction qui retourne n'importe quelle valeur
* @returns une nouvelle Map()
*/
export function toMap<T>(array: readonly T[]): Map<T, T>;
export function toMap<T, KF extends keyof T>(
array: readonly T[],
key: KF,
): Map<T[KF], T>;
export function toMap<T, KF extends (item: T) => any>(
array: readonly T[],
key: KF,
): Map<ReturnType<KF>, T>;
export function toMap<T, KF extends keyof T, VF extends keyof T>(
array: readonly T[],
key: KF,
value: VF,
): Map<T[KF], T[VF]>;
export function toMap<T, KF extends keyof T, VF extends (item: T) => any>(
array: readonly T[],
key: KF,
value: VF,
): Map<T[KF], ReturnType<VF>>;
export function toMap<
T,
KF extends (item: T) => any,
VF extends (item: T) => any,
>(
array: readonly T[],
key?: KF,
value?: VF,
): Map<ReturnType<KF>, ReturnType<VF>>;
export function toMap<T, KF extends (item: T) => any, VF extends keyof T>(
array: readonly T[],
key: KF,
value: VF,
): Map<ReturnType<KF>, T[VF]>;
export function toMap<
T,
KF extends keyof T | ((item: T) => any) | undefined,
KV extends keyof T | ((item: T) => any) | undefined,
>(array: readonly T[], key?: KF, value?: KV) {
type TKeyType = undefined extends KV
? T
: KF extends keyof T
? T[KF]
: KF extends (item: T) => infer K
? K
: never;
type TValueType = undefined extends KV
? T
: KV extends (item: T) => infer VF
? VF
: KV extends keyof T
? T[KV]
: never;
if (!array?.length) return new Map<TKeyType, TValueType>();
const keyFn: (item: T) => TKeyType =
typeof key === 'function'
? key
: key !== undefined && typeof array[0] === 'object' && array[0] !== null
? item => item[key as keyof T]
: item => item;
const valueFn: (item: T) => TValueType =
typeof value === 'function'
? value
: value !== undefined && typeof array[0] === 'object' && array[0] !== null
? item => item[value as keyof T]
: item => item;
// Créer la map et parcourir le tableau une seule fois
const map = new Map<TKeyType, TValueType>();
array?.forEach(item => {
map.set(keyFn(item), valueFn(item));
});
return map;
}
Ce que vous avez accompli
Vous avez transformé un simple one-liner en un utilitaire puissant et réutilisable! Bien que votre fonction toMap soit plus complexe que new Map(data.map(x => [x.key, x])), vous avez gagné:
- Meilleure lisibilité - Votre intention est très claire lorsque vous écrivez
toMap(users, 'id') - Sécurité des types - L'inférence complète de TypeScript vous protège des erreurs d'exécution
- Flexibilité - Prise en charge à la fois des noms de propriétés et des fonctions personnalisées
- Réutilisabilité - Une fonction gère tous vos besoins de conversion tableau-vers-dictionnaire
Vous avez également appris des concepts TypeScript avancés que vous pouvez appliquer à d'autres projets: les types conditionnels, l'inférence de types et la construction d'API flexibles. Ces compétences feront de vous un développeur TypeScript plus efficace.
