Ontgrendel de kracht van TypeScript met geavanceerde conditionele en mapped types. Leer flexibele, type-veilige applicaties te bouwen die zich aanpassen aan complexe datastructuren. Beheers de kunst van het schrijven van echt dynamische TypeScript-code.
Geavanceerde TypeScript Patronen: Meesterschap in Conditionele en Mapped Types
De kracht van TypeScript ligt in het vermogen om sterke typering te bieden, waardoor u fouten vroegtijdig kunt opsporen en beter onderhoudbare code kunt schrijven. Hoewel basistypes zoals string
, number
, en boolean
fundamenteel zijn, ontsluiten de geavanceerde functies van TypeScript, zoals conditionele en mapped types, een nieuwe dimensie van flexibiliteit en typeveiligheid. Deze uitgebreide gids duikt diep in deze krachtige concepten en voorziet u van de kennis om echt dynamische en aanpasbare TypeScript-applicaties te creëren.
Wat zijn Conditionele Types?
Met conditionele types kunt u types definiëren die afhankelijk zijn van een voorwaarde, vergelijkbaar met een ternaire operator in JavaScript (condition ? trueValue : falseValue
). Ze stellen u in staat om complexe typerelaties uit te drukken op basis van of een type aan een specifieke beperking voldoet.
Syntaxis
De basissyntaxis voor een conditioneel type is:
T extends U ? X : Y
T
: Het type dat wordt gecontroleerd.U
: Het type waartegen wordt gecontroleerd.extends
: Het sleutelwoord dat een subtype-relatie aangeeft.X
: Het type dat wordt gebruikt alsT
toewijsbaar is aanU
.Y
: Het type dat wordt gebruikt alsT
niet toewijsbaar is aanU
.
In wezen, als T extends U
evalueert naar true, resulteert het type in X
; anders resulteert het in Y
.
Praktische Voorbeelden
1. Het Bepalen van het Type van een Functieparameter
Stel dat u een type wilt maken dat bepaalt of een functieparameter een string of een number is:
type ParamType<T> = T extends string ? string : number;
function processValue(value: ParamType<string | number>): void {
if (typeof value === "string") {
console.log("Value is a string:", value);
} else {
console.log("Value is a number:", value);
}
}
processValue("hello"); // Output: Value is a string: hello
processValue(123); // Output: Value is a number: 123
In dit voorbeeld is ParamType<T>
een conditioneel type. Als T
een string is, wordt het type string
; anders wordt het number
. De processValue
-functie accepteert ofwel een string of een number op basis van dit conditionele type.
2. Return-type Extraheren op basis van Input-type
Stel u een scenario voor waarin u een functie heeft die verschillende types retourneert op basis van de input. Conditionele types kunnen u helpen het juiste return-type te definiëren:
interface StringProcessor {
process(input: string): number;
}
interface NumberProcessor {
process(input: number): string;
}
type Processor<T> = T extends string ? StringProcessor : NumberProcessor;
function createProcessor<T extends string | number>(input: T): Processor<T> {
if (typeof input === "string") {
return { process: (input: string) => input.length } as Processor<T>;
} else {
return { process: (input: number) => input.toString() } as Processor<T>;
}
}
const stringProcessor = createProcessor("example");
const numberProcessor = createProcessor(42);
console.log(stringProcessor.process("example")); // Output: 7
console.log(numberProcessor.process(42)); // Output: "42"
Hier selecteert het Processor<T>
-type conditioneel ofwel StringProcessor
of NumberProcessor
op basis van het type van de input. Dit zorgt ervoor dat de createProcessor
-functie het juiste type processorobject retourneert.
3. Gediscrimineerde Unions
Conditionele types zijn extreem krachtig bij het werken met gediscrimineerde unions. Een gediscrimineerde union is een union-type waarbij elk lid een gemeenschappelijke, singleton type-eigenschap heeft (de discriminant). Dit stelt u in staat om het type te verfijnen op basis van de waarde van die eigenschap.
interface Square {
kind: "square";
size: number;
}
interface Circle {
kind: "circle";
radius: number;
}
type Shape = Square | Circle;
type Area<T extends Shape> = T extends { kind: "square" } ? number : string;
function calculateArea(shape: Shape): Area<typeof shape> {
if (shape.kind === "square") {
return shape.size * shape.size;
} else {
return Math.PI * shape.radius * shape.radius;
}
}
const mySquare: Square = { kind: "square", size: 5 };
const myCircle: Circle = { kind: "circle", radius: 3 };
console.log(calculateArea(mySquare)); // Output: 25
console.log(calculateArea(myCircle)); // Output: 28.274333882308138
In dit voorbeeld is het Shape
-type een gediscrimineerde union. Het Area<T>
-type gebruikt een conditioneel type om te bepalen of de vorm een vierkant of een cirkel is, en retourneert een number
voor vierkanten en een string
voor cirkels (hoewel u in een praktijkscenario waarschijnlijk consistente return-types zou willen, demonstreert dit het principe).
Belangrijkste Punten over Conditionele Types
- Maken het mogelijk om types te definiëren op basis van voorwaarden.
- Verbeteren de typeveiligheid door complexe typerelaties uit te drukken.
- Zijn nuttig voor het werken met functieparameters, return-types en gediscrimineerde unions.
Wat zijn Mapped Types?
Mapped types bieden een manier om bestaande types te transformeren door hun eigenschappen te doorlopen. Ze stellen u in staat om nieuwe types te creëren op basis van de eigenschappen van een ander type, waarbij u aanpassingen toepast zoals het optioneel of readonly maken van eigenschappen, of het veranderen van hun types.
Syntaxis
De algemene syntaxis voor een mapped type is:
type NewType<T> = {
[K in keyof T]: ModifiedType;
};
T
: Het input-type.keyof T
: Een type-operator die een union van alle eigenschapssleutels inT
retourneert.K in keyof T
: Itereert over elke sleutel inkeyof T
en wijst elke sleutel toe aan de typevariabeleK
.ModifiedType
: Het type waarnaar elke eigenschap wordt gemapt. Dit kan conditionele types of andere type-transformaties omvatten.
Praktische Voorbeelden
1. Eigenschappen Optioneel Maken
U kunt een mapped type gebruiken om alle eigenschappen van een bestaand type optioneel te maken:
interface User {
id: number;
name: string;
email: string;
}
type PartialUser = {
[K in keyof User]?: User[K];
};
const partialUser: PartialUser = {
name: "John Doe",
}; // Geldig, aangezien 'id' en 'email' optioneel zijn
Hier is PartialUser
een mapped type dat over de sleutels van de User
-interface itereert. Voor elke sleutel K
maakt het de eigenschap optioneel door de ?
-modifier toe te voegen. De User[K]
haalt het type van de eigenschap K
op uit de User
-interface.
2. Eigenschappen Readonly Maken
Op dezelfde manier kunt u alle eigenschappen van een bestaand type readonly maken:
interface Product {
id: number;
name: string;
price: number;
}
type ReadonlyProduct = {
readonly [K in keyof Product]: Product[K];
};
const readonlyProduct: ReadonlyProduct = {
id: 123,
name: "Example Product",
price: 25.00,
};
// readonlyProduct.price = 30.00; // Fout: Kan niet toewijzen aan 'price' omdat het een read-only eigenschap is.
In dit geval is ReadonlyProduct
een mapped type dat de readonly
-modifier toevoegt aan elke eigenschap van de Product
-interface.
3. Eigenschapstypes Transformeren
Mapped types kunnen ook worden gebruikt om de types van eigenschappen te transformeren. U kunt bijvoorbeeld een type maken dat alle string-eigenschappen omzet naar numbers:
interface Config {
apiUrl: string;
timeout: string;
maxRetries: number;
}
type NumericConfig = {
[K in keyof Config]: Config[K] extends string ? number : Config[K];
};
const numericConfig: NumericConfig = {
apiUrl: 123, // Moet een number zijn vanwege de mapping
timeout: 456, // Moet een number zijn vanwege de mapping
maxRetries: 3,
};
Dit voorbeeld demonstreert het gebruik van een conditioneel type binnen een mapped type. Voor elke eigenschap K
controleert het of het type van Config[K]
een string is. Als dat zo is, wordt het type gemapt naar number
; anders blijft het ongewijzigd.
4. Key Remapping (sinds TypeScript 4.1)
TypeScript 4.1 introduceerde de mogelijkheid om sleutels binnen mapped types te her-mappen met het as
-sleutelwoord. Dit stelt u in staat om nieuwe types te creëren met verschillende eigenschapsnamen op basis van het oorspronkelijke type.
interface Event {
eventId: string;
eventName: string;
eventDate: Date;
}
type TransformedEvent = {
[K in keyof Event as `new${Capitalize<string&K>}`]: Event[K];
};
// Resultaat:
// {
// newEventId: string;
// newEventName: string;
// newEventDate: Date;
// }
//Capitalize-functie gebruikt om de eerste letter een hoofdletter te maken
type Capitalize<S extends string> = Uppercase<string&S> extends never ? string : `$Capitalize<S>`;
//Gebruik met een echt object
const myEvent: TransformedEvent = {
newEventId: "123",
newEventName: "New Name",
newEventDate: new Date()
};
Hier her-mapt het TransformedEvent
-type elke sleutel K
naar een nieuwe sleutel met het voorvoegsel "new" en een hoofdletter. De `Capitalize` utility-functie zorgt ervoor dat de eerste letter van de sleutel een hoofdletter wordt. De `string & K`-intersectie zorgt ervoor dat we alleen met string-sleutels werken en dat we het juiste letterlijke type van K krijgen. Key remapping opent krachtige mogelijkheden voor het transformeren en aanpassen van types aan specifieke behoeften. Hiermee kunt u sleutels hernoemen, filteren of wijzigen op basis van complexe logica.
Belangrijkste Punten over Mapped Types
- Maken het mogelijk om bestaande types te transformeren door hun eigenschappen te doorlopen.
- Laten toe om eigenschappen optioneel, readonly te maken, of hun types te veranderen.
- Zijn nuttig voor het creëren van nieuwe types op basis van de eigenschappen van een ander type.
- Key remapping (geïntroduceerd in TypeScript 4.1) biedt nog meer flexibiliteit bij type-transformaties.
Het Combineren van Conditionele en Mapped Types
De ware kracht van conditionele en mapped types komt naar voren wanneer u ze combineert. Dit stelt u in staat om zeer flexibele en expressieve typedefinities te creëren die zich kunnen aanpassen aan een breed scala van scenario's.Voorbeeld: Eigenschappen Filteren op Type
Stel dat u een type wilt maken dat de eigenschappen van een object filtert op basis van hun type. U zou bijvoorbeeld alleen de string-eigenschappen uit een object willen extraheren.
interface Data {
name: string;
age: number;
city: string;
country: string;
isEmployed: boolean;
}
type StringProperties<T> = {
[K in keyof T as T[K] extends string ? K : never]: T[K];
};
type StringData = StringProperties<Data>;
// Resultaat:
// {
// name: string;
// city: string;
// country: string;
// }
const stringData: StringData = {
name: "John",
city: "New York",
country: "USA",
};
In dit voorbeeld gebruikt het StringProperties<T>
-type een mapped type met key remapping en een conditioneel type. Voor elke eigenschap K
controleert het of het type van T[K]
een string is. Als dat zo is, wordt de sleutel behouden; anders wordt deze gemapt naar never
, waardoor deze effectief wordt uitgefilterd. never
als een gemapte typesleutel verwijdert deze uit het resulterende type. Dit zorgt ervoor dat alleen string-eigenschappen worden opgenomen in het StringData
-type.
Utility Types in TypeScript
TypeScript biedt verschillende ingebouwde utility types die gebruikmaken van conditionele en mapped types om veelvoorkomende type-transformaties uit te voeren. Het begrijpen van deze utility types kan uw code aanzienlijk vereenvoudigen en de typeveiligheid verbeteren.
Veelvoorkomende Utility Types
Partial<T>
: Maakt alle eigenschappen vanT
optioneel.Readonly<T>
: Maakt alle eigenschappen vanT
readonly.Required<T>
: Maakt alle eigenschappen vanT
verplicht (verwijdert de?
-modifier).Pick<T, K extends keyof T>
: Selecteert een set eigenschappenK
uitT
.Omit<T, K extends keyof T>
: Verwijdert een set eigenschappenK
uitT
.Record<K extends keyof any, 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>
: Haalt de parameters van een functietypeT
op in een tuple.ReturnType<T>
: Haalt het return-type van een functietypeT
op.InstanceType<T>
: Haalt het instancetype van een constructorfunctietypeT
op.ThisType<T>
: Dient als een markering voor het contextuelethis
-type.
Deze utility types zijn gebouwd met behulp van conditionele en mapped types, wat de kracht en flexibiliteit van deze geavanceerde TypeScript-functies aantoont. Bijvoorbeeld, Partial<T>
is als volgt gedefinieerd:
type Partial<T> = {
[P in keyof T]?: T[P];
};
Best Practices voor het Gebruik van Conditionele en Mapped Types
Hoewel conditionele en mapped types krachtig zijn, kunnen ze uw code ook complexer maken als ze niet zorgvuldig worden gebruikt. Hier zijn enkele best practices om in gedachten te houden:
- Houd het Eenvoudig: Vermijd overdreven complexe conditionele en mapped types. Als een typedefinitie te ingewikkeld wordt, overweeg dan om deze op te splitsen in kleinere, beter beheersbare delen.
- Gebruik Betekenisvolle Namen: Geef uw conditionele en mapped types beschrijvende namen die hun doel duidelijk aangeven.
- Documenteer Uw Types: Voeg commentaar toe om de logica achter uw conditionele en mapped types uit te leggen, vooral als ze complex zijn.
- Maak Gebruik van Utility Types: Controleer voordat u een aangepast conditioneel of mapped type maakt, of een ingebouwd utility type hetzelfde resultaat kan bereiken.
- Test Uw Types: Zorg ervoor dat uw conditionele en mapped types zich gedragen zoals verwacht door unit tests te schrijven die verschillende scenario's dekken.
- Houd Rekening met Prestaties: Complexe typeberekeningen kunnen de compilatietijden beïnvloeden. Wees u bewust van de prestatie-implicaties van uw typedefinities.
Conclusie
Conditionele en mapped types zijn essentiële hulpmiddelen om TypeScript te beheersen. Ze stellen u in staat om zeer flexibele, type-veilige en onderhoudbare applicaties te creëren die zich aanpassen aan complexe datastructuren en dynamische vereisten. Door de concepten die in deze gids zijn besproken te begrijpen en toe te passen, kunt u het volledige potentieel van TypeScript ontsluiten en robuustere en schaalbaardere code schrijven. Terwijl u TypeScript verder verkent, vergeet dan niet te experimenteren met verschillende combinaties van conditionele en mapped types om nieuwe manieren te ontdekken om uitdagende typeproblemen op te lossen. De mogelijkheden zijn werkelijk eindeloos.