Erfahren Sie, wie Sie eine TypeScript-Funktion erstellen, die Arrays in typsichere Dictionaries umwandelt, und meistern Sie bedingte Typen mit Typinferenz.
Haben Sie jemals schnell ein Array in ein Lookup-Dictionary umwandeln müssen? Während TypeScript die grundlegenden Werkzeuge bereitstellt, finden Sie sich oft beim Schreiben von sich wiederholendem Mapping-Code wieder. Hier ist der Standardansatz:
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') },
];
// Erstellen Sie eine Map mit der Standard-Map und Array.map
const dict = new Map(data.map(x => [x.key, x]))
// ^ const dict: Map<number, Person>
Auch wenn dies funktioniert, werden Sie schnell feststellen, dass es sich weitschweifig und repetitiv anfühlt. Was wäre, wenn Sie intuitiveren Code schreiben könnten, der sowohl flexibel als auch typsicher ist? Die toMap-Funktion unten bietet Ihnen genau das - sie unterstützt sowohl Eigenschaftsnamen als auch benutzerdefinierte Funktionen:
// Erstellen Sie eine Map mit der 'key'-Eigenschaft
const map1 = toMap(data, 'key');
// ^ const map1: Map<number, Person>
// Erstellen Sie eine Map mit der 'key'-Funktion
const map2 = toMap(data, x => x.key);
// ^ const map2: Map<number, Person>
// Erstellen Sie eine Map mit der 'key'-Eigenschaft und 'name'-Eigenschaft
const map3 = toMap(data, 'key', 'name');
// ^ const map3: Map<number, string>
// Erstellen Sie eine Map mit der 'key'-Funktion und 'value'-Funktion
const map4 = toMap(data, x => x.key, x => x.name);
// ^ const map4: Map<number, string>
// Erstellen Sie eine Map mit benutzerdefinierten Funktionen
const map5 = toMap(data, x => x.birthday, x => `Name: ${x.name}`);
// ^ const map5: Map<Date, string>
// Erstellen Sie eine Map ohne Parameter
const map6 = toMap(data);
// ^ const map6: Map<Person, Person>
Bereit, dieses leistungsstarke Hilfsprogramm zu erstellen? Folgen Sie uns, während wir es Schritt für Schritt aufbauen. Jede Iteration wird Ihnen neue TypeScript-Konzepte beibringen, die Sie auf Ihre eigenen Projekte anwenden können: (Klicken Sie auf die Ränder des Codeblocks, um vorwärts oder rückwärts durch die Versionen zu gehen)
// Beginnen Sie einfach - Sie werden die Komplexität schrittweise aufbauen
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>
// Wir fügen etwas Abstand hinzu, um die Schritte zu durchlaufen, ohne dass Zeilen herumspringen
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>
// Jetzt lernen Sie bedingte Typen - erweitern Sie den Schlüsselparameter, um Eigenschaftsnamen zu akzeptieren// und verschieben Sie den Schlüsseltyp zu den Generics, damit Sie später darauf verweisen könnenexport function toMap<
T,
K,
KF extends keyof T | ((item: T) => K) >(
array: readonly T[],
key: KF,
) {
// - Erstellen Sie Ihren ersten bedingten Typ - er wählt zwischen Eigenschaftstyp oder Funktions-Rückgabetyp type TKeyType = KF extends keyof T ? T[KF] : KF extends ((item: T) => K) ? K : never;
if (!array?.length) return new Map<TKeyType, T>();
// - Sie werden hier auf einen TypeScript-Fehler stoßen - 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, wenn das Array primitive Werte enthält
return new Map<TKeyType, T>(array.map(item => [keyFn(item), item]));
}
// - Noch nicht die Typsicherheit, die Sie wollenconst dict = toMap(data, x => x.birthday); // ^ const dict: Map<unknown, Person>// So lösen Sie den Fehler - verwenden Sie stattdessen Typinferenz// - Entfernen Sie den K-Typparameter und lassen Sie TypeScript ihn ableitenexport function toMap<
T,
KF extends keyof T | ((item: T) => any) >(
array: readonly T[],
key: KF,
) {
// - Lassen Sie TypeScript den Rückgabetyp für Sie ableiten 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]));
}
// - Perfekt! Jetzt erhalten Sie die richtige Typinferenzconst dict = toMap(data, x => x.birthday); // ^ const dict: Map<Date, Person>// Machen Sie die Funktion flexibler - optionale Parameter ermöglichen Ihnen mehr zu tun
export function toMap<
T,
KF extends keyof T | ((item: T) => any) | undefined, >(
array: readonly T[],
key?: KF,
) {
// - Behandeln Sie den optionalen Fall in Ihren bedingten Typen, indem Sie T zurückgeben 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>// Wenden Sie an, was Sie gelernt haben - fügen Sie einen Wertparameter mit demselben Muster hinzu
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>// Ihr Endergebnis - ein leistungsstarkes, typsicheres Hilfsprogramm, das Sie überall verwenden können
/**
* Hilft beim Erstellen einer Map (Dictionary) aus einer Liste von Elementen.
* Für den Schlüssel und die Werte können Sie entweder ein keyof T oder eine Funktion angeben, die den Wert zurückgibt.
* Wenn Sie keinen Wert angeben, wird das gesamte Element als Wert verwendet.
* @param array das zu konvertierende Array
* @param key ein keyof eines Array-Elements | eine Funktion, die einen Wert zurückgibt, der den Schlüssel des Dictionaries darstellt
* @param value ein keyof eines Array-Elements | eine Funktion, die einen beliebigen Wert zurückgibt
* @returns eine neue 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;
// Erstellen Sie die Map und durchlaufen Sie das Array nur einmal
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>
Möchten Sie sehen, wie gründlich diese Funktion getestet wird? Schauen Sie sich die vollständige Testdatei hier an toMap.tests.ts.
Arbeiten mit deaktiviertem strictNullChecks
Wenn strictNullChecks deaktiviert ist (oder der Strict-Modus selbst deaktiviert ist), wird undefined allen Typen zuweisbar, was unseren bedingten Typ zerstört.
In diesem Szenario hat die resultierende Map immer die Schlüssel- und Werttypen T, unabhängig davon, welche Parameter an toMap übergeben werden.
Um diese Einschränkung zu bewältigen, können Sie Funktionsüberladungen verwenden, um explizite Typsignaturen bereitzustellen, die den verschiedenen Parameterkombinationen entsprechen und so Typsicherheit gewährleisten, auch wenn strikte Nullprüfungen deaktiviert sind.
So können Sie dies implementieren:
/**
* Hilft beim Erstellen einer Map (Dictionary) aus einer Liste von Elementen.
* Für den Schlüssel und die Werte können Sie entweder ein keyof T oder eine Funktion angeben, die den Wert zurückgibt.
* Wenn Sie keinen Wert angeben, wird das gesamte Element als Wert verwendet.
*
* @param array das zu konvertierende Array
* @param key ein keyof eines Array-Elements | eine Funktion, die einen Wert zurückgibt, der den Schlüssel des Dictionaries darstellt
* @param value ein keyof eines Array-Elements | eine Funktion, die einen beliebigen Wert zurückgibt
* @returns eine neue 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;
// Erstellen Sie die Map und durchlaufen Sie das Array nur einmal
const map = new Map<TKeyType, TValueType>();
array?.forEach(item => {
map.set(keyFn(item), valueFn(item));
});
return map;
}
Was Sie erreicht haben
Sie haben einen einfachen Einzeiler in ein leistungsstarkes, wiederverwendbares Hilfsprogramm verwandelt! Auch wenn Ihre toMap-Funktion komplexer ist als new Map(data.map(x => [x.key, x])), haben Sie Folgendes gewonnen:
- Bessere Lesbarkeit - Ihre Absicht ist kristallklar, wenn Sie
toMap(users, 'id')schreiben - Typsicherheit - Vollständige TypeScript-Inferenz schützt Sie vor Laufzeitfehlern
- Flexibilität - Unterstützung für sowohl Eigenschaftsnamen als auch benutzerdefinierte Funktionen
- Wiederverwendbarkeit - Eine Funktion bewältigt alle Ihre Array-zu-Dictionary-Anforderungen
Sie haben auch einige fortgeschrittene TypeScript-Konzepte gelernt, die Sie auf andere Projekte anwenden können: bedingte Typen, Typinferenz und den Aufbau flexibler APIs. Diese Fähigkeiten werden Sie zu einem effektiveren TypeScript-Entwickler machen.
