Leer hoe je een typescript functie maakt die arrays omzet naar type-veilige dictionaries en beheers conditionele types met type inferentie.
Heb je ooit snel een array naar een lookup dictionary moeten converteren? Hoewel TypeScript de basistools biedt, schrijf je vaak repetitieve mapping code. Dit is de standaard aanpak:
interface Persoon { sleutel: number, naam: string, verjaardag: Date }
const data: Persoon[] = [
{ sleutel: 1, naam: 'Rupert', verjaardag: new Date('1986-04-10') },
{ sleutel: 2, naam: 'Anna', verjaardag: new Date('1989-10-10') },
];
// Maak een map met de standaard Map en Array.map
const dict = new Map(data.map(x => [x.sleutel, x]))
// ^ const dict: Map<number, Persoon>
Hoewel dit werkt, merk je snel dat het uitgebreid en repetitief aanvoelt. Wat als je meer intuïtieve code kon schrijven die zowel flexibel als type-veilig is? De toMap functie hieronder geeft je precies dat - met ondersteuning voor zowel property namen als aangepaste functies:
// Maak een map met de 'sleutel' property
const map1 = toMap(data, 'sleutel');
// ^ const map1: Map<number, Persoon>
// Maak een map met de 'sleutel' functie
const map2 = toMap(data, x => x.sleutel);
// ^ const map2: Map<number, Persoon>
// Maak een map met de 'sleutel' property en 'waarde' property
const map3 = toMap(data, 'sleutel', 'naam');
// ^ const map3: Map<number, string>
// Maak een map met de 'sleutel' functie en 'waarde' functie
const map4 = toMap(data, x => x.sleutel, x => x.naam);
// ^ const map4: Map<number, string>
// Maak een map met aangepaste functies
const map5 = toMap(data, x => x.verjaardag, x => `Naam: ${x.naam}`);
// ^ const map5: Map<Date, string>
// Maak een map zonder parameters
const map6 = toMap(data);
// ^ const map6: Map<Persoon, Persoon>
Klaar om deze krachtige utility te bouwen? Volg mee terwijl we het stap voor stap construeren. Elke iteratie leert je nieuwe TypeScript concepten die je kunt toepassen in je eigen projecten: (klik op de randen van het codeblok om vooruit of achteruit te gaan door de versies)
// Begin simpel - je bouwt de complexiteit geleidelijk op
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.verjaardag);
// ^ const dict: Map<Date, Persoon>
// We voegen wat ruimte toe om door de stappen te gaan zonder dat regels rondspringen
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.verjaardag);
// ^ const dict: Map<Date, Persoon>
// Nu leer je conditionele types - breid de key parameter uit om property namen te accepteren// en verplaats het key type naar generics zodat je het later kunt refererenexport function toMap<
T,
K,
KF extends keyof T | ((item: T) => K) >(
array: readonly T[],
key: KF,
) {
// - Maak je eerste conditionele type - het kiest tussen property type of functie return type type TKeyType = KF extends keyof T ? T[KF] : KF extends ((item: T) => K) ? K : never;
if (!array?.length) return new Map<TKeyType, T>();
// - Je krijgt hier een TypeScript fout - Type 'K' is niet toewijsbaar aan 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 wanneer de array primitieve waarden bevat
return new Map<TKeyType, T>(array.map(item => [keyFn(item), item]));
}
// - Nog niet de type veiligheid die je wiltconst dict = toMap(data, x => x.verjaardag); // ^ const dict: Map<unknown, Persoon>// Zo los je de fout op - gebruik type inferentie in plaats daarvan// - Verwijder de K type parameter en laat TypeScript het afleidenexport function toMap<
T,
KF extends keyof T | ((item: T) => any) >(
array: readonly T[],
key: KF,
) {
// - Laat TypeScript het return type voor je afleiden 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]));
}
// - Perfect! Nu krijg je correcte type inferentieconst dict = toMap(data, x => x.verjaardag); // ^ const dict: Map<Date, Persoon>// Maak de functie flexibeler - optionele parameters stellen je in staat meer te doen
export function toMap<
T,
KF extends keyof T | ((item: T) => any) | undefined, >(
array: readonly T[],
key?: KF,
) {
// - Behandel het optionele geval in je conditionele types, door T te retourneren 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<Persoon, Persoon>// Pas toe wat je hebt geleerd - voeg value parameter toe met hetzelfde patroon
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, 'sleutel', 'verjaardag');
// ^ const dict: Map<number, Date>// Je eindresultaat - een krachtige, type-veilige utility die je overal kunt gebruiken
/**
* Helpt om een map (dictionary) te maken van een lijst met items.
* Voor de key en values kun je ofwel een keyof T opgeven of een functie die de waarde retourneert.
* Als je geen waarde opgeeft, wordt het hele item als waarde gebruikt.
* @param array de array om te converteren
* @param key een keyof een array item | een functie die een waarde retourneert die de sleutel van de dictionary vertegenwoordigt
* @param value een keyof een array item | een functie die een willekeurige waarde retourneert
* @returns een nieuwe 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;
// Maak de map en doorloop de array slechts één keer
const map = new Map<TKeyType, TValueType>();
array.forEach((item) => {
map.set(keyFn(item), valueFn(item));
});
return map;
}
const dict = toMap(data, 'sleutel', x => x.verjaardag);
// ^ const dict: Map<number, Date>
Wil je zien hoe grondig deze functie getest is? Bekijk het volledige testbestand hier toMap.tests.ts.
Werken met strictNullChecks uitgeschakeld
Wanneer strictNullChecks is uitgeschakeld (of strict mode zelf is uitgeschakeld), wordt undefined toewijsbaar aan alle types, wat ons conditionele type breekt.
In dit scenario, ongeacht de parameters die aan toMap worden doorgegeven, zullen de resulterende map altijd de key en value types T hebben.
Om deze beperking te behandelen, kun je functie overloads gebruiken om expliciete type signatures te bieden die overeenkomen met de verschillende parameter combinaties, waardoor type veiligheid wordt gegarandeerd, zelfs wanneer strikte null checks zijn uitgeschakeld.
Zo kun je dit implementeren:
/**
* Helpt om een map (dictionary) te maken van een lijst met items.
* Voor de key en values kun je ofwel een keyof T opgeven of een functie die de waarde retourneert.
* Als je geen waarde opgeeft, wordt het hele item als waarde gebruikt.
*
* @param array de array om te converteren
* @param key een keyof een array item | een functie die een waarde retourneert die de sleutel van de dictionary vertegenwoordigt
* @param value een keyof een array item | een functie die een willekeurige waarde retourneert
* @returns een nieuwe 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;
// Maak de map en doorloop de array slechts één keer
const map = new Map<TKeyType, TValueType>();
array?.forEach(item => {
map.set(keyFn(item), valueFn(item));
});
return map;
}
Wat Je Hebt Bereikt
Je hebt een simpele one-liner getransformeerd naar een krachtige, herbruikbare utility! Hoewel je toMap functie complexer is dan new Map(data.map(x => [x.sleutel, x])), heb je het volgende bereikt:
- Betere leesbaarheid - Je intentie is kristalhelder wanneer je
toMap(gebruikers, 'id')schrijft - Type veiligheid - Volledige TypeScript inferentie beschermt je tegen runtime fouten
- Flexibiliteit - Ondersteuning voor zowel property namen als aangepaste functies
- Herbruikbaarheid - Eén functie behandelt al je array-naar-dictionary behoeften
Je hebt ook enkele geavanceerde TypeScript concepten geleerd die je kunt toepassen in andere projecten: conditionele types, type inferentie, en het bouwen van flexibele API's. Deze vaardigheden maken je een effectievere TypeScript ontwikkelaar.
