Een uitgebreide gids voor TypeScript's krachtige Mapped Types en Conditional Types, met praktische voorbeelden en geavanceerde toepassingen voor het bouwen van robuuste en type-veilige applicaties.
TypeScript's Mapped Types en Conditional Types de Baas Worden
TypeScript, een superset van JavaScript, biedt krachtige functies voor het bouwen van robuuste en onderhoudbare applicaties. Onder deze functies vallen Mapped Types en Conditional Types op als essentiële hulpmiddelen voor geavanceerde type-manipulatie. Deze gids biedt een uitgebreid overzicht van deze concepten, waarbij de syntaxis, praktische toepassingen en geavanceerde use cases worden verkend. Of u nu een ervaren TypeScript-ontwikkelaar bent of net aan uw reis begint, dit artikel zal u voorzien van de kennis om deze functies effectief te benutten.
Wat zijn Mapped Types?
Mapped Types stellen u in staat nieuwe types te creëren door bestaande te transformeren. Ze itereren over de eigenschappen van een bestaand type en passen op elke eigenschap een transformatie toe. Dit is met name handig voor het creëren van variaties op bestaande types, zoals het optioneel of alleen-lezen maken van alle eigenschappen.
Basissyntaxis
De syntaxis voor een Mapped Type is als volgt:
type NewType<T> = {
[K in keyof T]: Transformation;
};
T
: Het invoertype waarover u wilt mappen.K in keyof T
: Itereert over elke sleutel in het invoertypeT
.keyof T
creëert een union van alle eigenschapsnamen inT
, enK
vertegenwoordigt elke individuele sleutel tijdens de iteratie.Transformation
: De transformatie die u op elke eigenschap wilt toepassen. Dit kan het toevoegen van een modifier zijn (zoalsreadonly
of?
), het veranderen van het type, of iets heel anders.
Praktische Voorbeelden
Eigenschappen Alleen-Lezen Maken
Stel dat u een interface heeft die een gebruikersprofiel vertegenwoordigt:
interface UserProfile {
name: string;
age: number;
email: string;
}
U kunt een nieuw type creëren waarin alle eigenschappen alleen-lezen zijn:
type ReadOnlyUserProfile = {
readonly [K in keyof UserProfile]: UserProfile[K];
};
Nu heeft ReadOnlyUserProfile
dezelfde eigenschappen als UserProfile
, maar ze zijn allemaal alleen-lezen.
Eigenschappen Optioneel Maken
Op dezelfde manier kunt u alle eigenschappen optioneel maken:
type OptionalUserProfile = {
[K in keyof UserProfile]?: UserProfile[K];
};
OptionalUserProfile
heeft alle eigenschappen van UserProfile
, maar elke eigenschap is optioneel.
Eigenschapstypes Wijzigen
U kunt ook het type van elke eigenschap wijzigen. U kunt bijvoorbeeld alle eigenschappen transformeren naar strings:
type StringifiedUserProfile = {
[K in keyof UserProfile]: string;
};
In dit geval zullen alle eigenschappen in StringifiedUserProfile
van het type string
zijn.
Wat zijn Conditional Types?
Conditional Types stellen u in staat types te definiëren die afhankelijk zijn van een voorwaarde. Ze bieden een manier om type-relaties uit te drukken op basis van of een type aan een bepaalde beperking voldoet. Dit is vergelijkbaar met een ternaire operator in JavaScript, maar dan voor types.
Basissyntaxis
De syntaxis voor een Conditional Type is als volgt:
T extends U ? X : Y
T
: Het type dat wordt gecontroleerd.U
: Het type dat doorT
wordt uitgebreid (de voorwaarde).X
: Het type dat wordt geretourneerd alsT
U
uitbreidt (de voorwaarde is waar).Y
: Het type dat wordt geretourneerd alsT
U
niet uitbreidt (de voorwaarde is onwaar).
Praktische Voorbeelden
Bepalen of een Type een String is
Laten we een type creëren dat string
retourneert als het invoertype een string is, en anders number
:
type StringOrNumber<T> = T extends string ? string : number;
type Result1 = StringOrNumber<string>; // string
type Result2 = StringOrNumber<number>; // number
type Result3 = StringOrNumber<boolean>; // number
Type Extraheren uit een Union
U kunt conditional types gebruiken om een specifiek type uit een union type te extraheren. Bijvoorbeeld om non-nullable types te extraheren:
type NonNullable<T> = T extends null | undefined ? never : T;
type Result4 = NonNullable<string | null | undefined>; // string
Hier, als T
null
of undefined
is, wordt het type never
, wat vervolgens wordt uitgefilterd door de vereenvoudiging van union types in TypeScript.
Types Afleiden (Infer)
Conditional types kunnen ook worden gebruikt om types af te leiden met het infer
sleutelwoord. Hiermee kunt u een type extraheren uit een complexere type-structuur.
type ReturnType<T extends (...args: any) => any> = T extends (...args: any) => infer R ? R : any;
function myFunction(x: number): string {
return x.toString();
}
type Result5 = ReturnType<typeof myFunction>; // string
In dit voorbeeld extraheert ReturnType
het return-type van een functie. Het controleert of T
een functie is die willekeurige argumenten aanneemt en een type R
retourneert. Zo ja, dan retourneert het R
; anders retourneert het any
.
Mapped Types en Conditional Types Combineren
De ware kracht van Mapped Types en Conditional Types komt voort uit hun combinatie. Dit stelt u in staat om zeer flexibele en expressieve type-transformaties te creëren.
Voorbeeld: Deep Readonly
Een veelvoorkomende toepassing is het creëren van een type dat alle eigenschappen van een object, inclusief geneste eigenschappen, alleen-lezen maakt. Dit kan worden bereikt met een recursief conditional type.
type DeepReadonly<T> = {
readonly [K in keyof T]: T[K] extends object ? DeepReadonly<T[K]> : T[K];
};
interface Company {
name: string;
address: {
street: string;
city: string;
};
}
type ReadonlyCompany = DeepReadonly<Company>;
Hier past DeepReadonly
de readonly
-modifier recursief toe op alle eigenschappen en hun geneste eigenschappen. Als een eigenschap een object is, roept het recursief DeepReadonly
aan op dat object. Anders past het simpelweg de readonly
-modifier toe op de eigenschap.
Voorbeeld: Eigenschappen Filteren op Type
Stel dat u een type wilt creëren dat alleen eigenschappen van een specifiek type bevat. U kunt Mapped Types en Conditional Types combineren om dit te bereiken.
type FilterByType<T, U> = {
[K in keyof T as T[K] extends U ? K : never]: T[K];
};
interface Person {
name: string;
age: number;
isEmployed: boolean;
}
type StringProperties = FilterByType<Person, string>; // { name: string; }
type NonStringProperties = Omit<Person, keyof StringProperties>;
In dit voorbeeld itereert FilterByType
over de eigenschappen van T
en controleert of het type van elke eigenschap U
uitbreidt. Zo ja, dan wordt de eigenschap opgenomen in het resulterende type; anders wordt deze uitgesloten door de sleutel te mappen naar never
. Let op het gebruik van "as" om sleutels opnieuw te mappen. Vervolgens gebruiken we `Omit` en `keyof StringProperties` om de string-eigenschappen uit de oorspronkelijke interface te verwijderen.
Geavanceerde Toepassingen en Patronen
Naast de basisvoorbeelden kunnen Mapped Types en Conditional Types worden gebruikt in meer geavanceerde scenario's om zeer aanpasbare en type-veilige applicaties te creëren.
Distributieve Conditional Types
Conditional types zijn distributief wanneer het type dat wordt gecontroleerd een union type is. Dit betekent dat de voorwaarde op elk lid van de union afzonderlijk wordt toegepast, en de resultaten vervolgens worden gecombineerd tot een nieuw union type.
type ToArray<T> = T extends any ? T[] : never;
type Result6 = ToArray<string | number>; // string[] | number[]
In dit voorbeeld wordt ToArray
toegepast op elk lid van de union string | number
afzonderlijk, wat resulteert in string[] | number[]
. Als de voorwaarde niet distributief was geweest, zou het resultaat (string | number)[]
zijn geweest.
Gebruik van Utility Types
TypeScript biedt verschillende ingebouwde utility types die gebruikmaken van Mapped Types en Conditional Types. Deze utility types kunnen worden gebruikt als bouwstenen voor complexere type-transformaties.
Partial<T>
: Maakt alle eigenschappen vanT
optioneel.Required<T>
: Maakt alle eigenschappen vanT
verplicht.Readonly<T>
: Maakt alle eigenschappen vanT
alleen-lezen.Pick<T, K>
: Selecteert een set eigenschappenK
uitT
.Omit<T, K>
: Verwijdert een set eigenschappenK
uitT
.Record<K, T>
: Construeert een type met een set eigenschappenK
van het typeT
.Exclude<T, U>
: Sluit vanT
alle types uit die toewijsbaar zijn aanU
.Extract<T, U>
: Extraheert uitT
alle types die toewijsbaar zijn aanU
.NonNullable<T>
: Sluitnull
enundefined
uit vanT
.Parameters<T>
: Verkrijgt de parameters van een functietypeT
.ReturnType<T>
: Verkrijgt het return-type van een functietypeT
.InstanceType<T>
: Verkrijgt het instance-type van een constructor-functietypeT
.
Deze utility types zijn krachtige hulpmiddelen die complexe type-manipulaties kunnen vereenvoudigen. U kunt bijvoorbeeld Pick
en Partial
combineren om een type te creëren dat alleen bepaalde eigenschappen optioneel maakt:
type Optional<T, K extends keyof T> = Omit<T, K> & Partial<Pick<T, K>>;
interface Product {
id: number;
name: string;
price: number;
description: string;
}
type OptionalDescriptionProduct = Optional<Product, "description">;
In dit voorbeeld heeft OptionalDescriptionProduct
alle eigenschappen van Product
, maar de eigenschap description
is optioneel.
Gebruik van Template Literal Types
Template Literal Types stellen u in staat types te creëren op basis van string literals. Ze kunnen worden gebruikt in combinatie met Mapped Types en Conditional Types om dynamische en expressieve type-transformaties te creëren. U kunt bijvoorbeeld een type creëren dat alle eigenschapsnamen voorziet van een specifiek voorvoegsel:
type Prefix<T, P extends string> = {
[K in keyof T as `${P}${string & K}`]: T[K];
};
interface Settings {
apiUrl: string;
timeout: number;
}
type PrefixedSettings = Prefix<Settings, "data_">;
In dit voorbeeld heeft PrefixedSettings
de eigenschappen data_apiUrl
en data_timeout
.
Best Practices en Overwegingen
- Houd het Simpel: Hoewel Mapped Types en Conditional Types krachtig zijn, kunnen ze uw code ook complexer maken. Probeer uw type-transformaties zo eenvoudig mogelijk te houden.
- Gebruik Utility Types: Maak waar mogelijk gebruik van de ingebouwde utility types van TypeScript. Ze zijn goed getest en kunnen uw code vereenvoudigen.
- Documenteer uw Types: Documenteer uw type-transformaties duidelijk, vooral als ze complex zijn. Dit helpt andere ontwikkelaars uw code te begrijpen.
- Test uw Types: Gebruik de type-checking van TypeScript om te controleren of uw type-transformaties werken zoals verwacht. U kunt unit tests schrijven om het gedrag van uw types te verifiëren.
- Houd Rekening met Prestaties: Complexe type-transformaties kunnen de prestaties van uw TypeScript-compiler beïnvloeden. Wees u bewust van de complexiteit van uw types en vermijd onnodige berekeningen.
Conclusie
Mapped Types en Conditional Types zijn krachtige functies in TypeScript die u in staat stellen zeer flexibele en expressieve type-transformaties te creëren. Door deze concepten te beheersen, kunt u de typeveiligheid, onderhoudbaarheid en algehele kwaliteit van uw TypeScript-applicaties verbeteren. Van eenvoudige transformaties zoals het optioneel of alleen-lezen maken van eigenschappen tot complexe recursieve transformaties en conditionele logica, bieden deze functies de hulpmiddelen die u nodig heeft om robuuste en schaalbare applicaties te bouwen. Blijf deze functies verkennen en ermee experimenteren om hun volledige potentieel te ontsluiten en een vaardigere TypeScript-ontwikkelaar te worden.
Terwijl u uw TypeScript-reis voortzet, vergeet dan niet gebruik te maken van de overvloed aan beschikbare bronnen, waaronder de officiële TypeScript-documentatie, online communities en open-sourceprojecten. Omarm de kracht van Mapped Types en Conditional Types, en u zult goed uitgerust zijn om zelfs de meest uitdagende type-gerelateerde problemen aan te pakken.