Frigør kraften i TypeScript med avancerede betingede og mappede typer. Lær at skabe fleksible, typesikre applikationer, der tilpasser sig komplekse datastrukturer. Mestr kunsten at skrive ægte dynamisk TypeScript-kode.
Avancerede TypeScript-mønstre: Mestring af betingede og mappede typer
Styrken ved TypeScript ligger i dets evne til at levere stærk typning, hvilket giver dig mulighed for at fange fejl tidligt og skrive mere vedligeholdelsesvenlig kode. Mens grundlæggende typer som string
, number
, og boolean
er fundamentale, åbner TypeScripts avancerede funktioner som betingede og mappede typer op for en ny dimension af fleksibilitet og typesikkerhed. Denne omfattende guide vil dykke ned i disse kraftfulde koncepter og udstyre dig med viden til at skabe ægte dynamiske og tilpasningsdygtige TypeScript-applikationer.
Hvad er betingede typer?
Betingede typer giver dig mulighed for at definere typer, der afhænger af en betingelse, ligesom en ternær operator i JavaScript (condition ? trueValue : falseValue
). De gør det muligt for dig at udtrykke komplekse typeforhold baseret på, om en type opfylder en specifik begrænsning.
Syntaks
Den grundlæggende syntaks for en betinget type er:
T extends U ? X : Y
T
: Typen, der kontrolleres.U
: Typen, der skal kontrolleres imod.extends
: Nøgleordet, der indikerer et subtype-forhold.X
: Typen, der skal bruges, hvisT
kan tildeles tilU
.Y
: Typen, der skal bruges, hvisT
ikke kan tildeles tilU
.
I bund og grund, hvis T extends U
evalueres til sand, bliver typen løst til X
; ellers bliver den løst til Y
.
Praktiske eksempler
1. Bestemmelse af typen på en funktionsparameter
Lad os sige, at du vil oprette en type, der bestemmer, om en funktionsparameter er en streng eller et tal:
type ParamType<T> = T extends string ? string : number;
function processValue(value: ParamType<string | number>): void {
if (typeof value === "string") {
console.log("Værdien er en streng:", value);
} else {
console.log("Værdien er et tal:", value);
}
}
processValue("hello"); // Output: Værdien er en streng: hello
processValue(123); // Output: Værdien er et tal: 123
I dette eksempel er ParamType<T>
en betinget type. Hvis T
er en streng, løses typen til string
; ellers løses den til number
. Funktionen processValue
accepterer enten en streng eller et tal baseret på denne betingede type.
2. Udtrækning af returtype baseret på inputtype
Forestil dig et scenarie, hvor du har en funktion, der returnerer forskellige typer baseret på input. Betingede typer kan hjælpe dig med at definere den korrekte returtype:
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"
Her vælger Processor<T>
-typen betinget enten StringProcessor
eller NumberProcessor
baseret på typen af input. Dette sikrer, at createProcessor
-funktionen returnerer den korrekte type processor-objekt.
3. Diskriminerede unioner
Betingede typer er ekstremt kraftfulde, når man arbejder med diskriminerede unioner. En diskrimineret union er en unionstype, hvor hvert medlem har en fælles singleton-typeegenskab (diskriminanten). Dette giver dig mulighed for at indsnævre typen baseret på værdien af den egenskab.
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
I dette eksempel er Shape
-typen en diskrimineret union. Area<T>
-typen bruger en betinget type til at bestemme, om formen er en firkant eller en cirkel, og returnerer et number
for firkanter og en string
for cirkler (selvom du i et virkeligt scenarie sandsynligvis ville ønske konsistente returtyper, demonstrerer dette princippet).
Vigtige pointer om betingede typer
- Muliggør definition af typer baseret på betingelser.
- Forbedrer typesikkerhed ved at udtrykke komplekse typeforhold.
- Er nyttige til at arbejde med funktionsparametre, returtyper og diskriminerede unioner.
Hvad er mappede typer?
Mappede typer giver en måde at transformere eksisterende typer på ved at mappe over deres egenskaber. De giver dig mulighed for at oprette nye typer baseret på egenskaberne i en anden type, og anvende modifikationer som at gøre egenskaber valgfrie, skrivebeskyttede eller ændre deres typer.
Syntaks
Den generelle syntaks for en mappet type er:
type NewType<T> = {
[K in keyof T]: ModifiedType;
};
T
: Inputtypen.keyof T
: En typeoperator, der returnerer en union af alle egenskabsnøgler iT
.K in keyof T
: Itererer over hver nøgle ikeyof T
og tildeler hver nøgle til typevariablenK
.ModifiedType
: Typen, som hver egenskab vil blive mappet til. Dette kan inkludere betingede typer eller andre typetransformationer.
Praktiske eksempler
1. Gøre egenskaber valgfri
Du kan bruge en mappet type til at gøre alle egenskaber i en eksisterende type valgfrie:
interface User {
id: number;
name: string;
email: string;
}
type PartialUser = {
[K in keyof User]?: User[K];
};
const partialUser: PartialUser = {
name: "John Doe",
}; // Gyldigt, da 'id' og 'email' er valgfrie
Her er PartialUser
en mappet type, der itererer over nøglerne i User
-interfacet. For hver nøgle K
gør den egenskaben valgfri ved at tilføje ?
-modifikatoren. User[K]
henter typen af egenskaben K
fra User
-interfacet.
2. Gøre egenskaber skrivebeskyttede
På samme måde kan du gøre alle egenskaber i en eksisterende type skrivebeskyttede:
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; // Fejl: Kan ikke tildele til 'price', fordi det er en skrivebeskyttet egenskab.
I dette tilfælde er ReadonlyProduct
en mappet type, der tilføjer readonly
-modifikatoren til hver egenskab i Product
-interfacet.
3. Transformering af egenskabstyper
Mappede typer kan også bruges til at transformere typerne af egenskaber. For eksempel kan du oprette en type, der konverterer alle strengegenskaber til tal:
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, // Skal være et tal på grund af mappingen
timeout: 456, // Skal være et tal på grund af mappingen
maxRetries: 3,
};
Dette eksempel demonstrerer brugen af en betinget type inden i en mappet type. For hver egenskab K
kontrollerer den, om typen af Config[K]
er en streng. Hvis den er det, mappes typen til number
; ellers forbliver den uændret.
4. Ommærkning af nøgler (siden TypeScript 4.1)
TypeScript 4.1 introducerede muligheden for at ommærke nøgler inden i mappede typer ved hjælp af as
-nøgleordet. Dette giver dig mulighed for at oprette nye typer med forskellige egenskabsnavne baseret på den oprindelige type.
interface Event {
eventId: string;
eventName: string;
eventDate: Date;
}
type TransformedEvent = {
[K in keyof Event as `new${Capitalize<string&K>}`]: Event[K];
};
// Resultat:
// {
// newEventId: string;
// newEventName: string;
// newEventDate: Date;
// }
//Capitalize-funktion brugt til at gøre det første bogstav stort
type Capitalize<S extends string> = Uppercase<string&S> extends never ? string : `$Capitalize<S>`;
//Brug med et faktisk objekt
const myEvent: TransformedEvent = {
newEventId: "123",
newEventName: "New Name",
newEventDate: new Date()
};
Her ommærker TransformedEvent
-typen hver nøgle K
til en ny nøgle med præfikset "new" og med stort begyndelsesbogstav. Capitalize
-utility-funktionen sikrer, at det første bogstav i nøglen er stort. string & K
-skæringen sikrer, at vi kun har at gøre med streng-nøgler, og at vi får den korrekte bogstavelige type fra K.
Ommærkning af nøgler åbner op for kraftfulde muligheder for at transformere og tilpasse typer til specifikke behov. Dette giver dig mulighed for at omdøbe, filtrere eller ændre nøgler baseret på kompleks logik.
Vigtige pointer om mappede typer
- Muliggør transformation af eksisterende typer ved at mappe over deres egenskaber.
- Tillader at gøre egenskaber valgfri, skrivebeskyttede eller at ændre deres typer.
- Er nyttige til at oprette nye typer baseret på en anden types egenskaber.
- Ommærkning af nøgler (introduceret i TypeScript 4.1) giver endnu større fleksibilitet i typetransformationer.
Kombinering af betingede og mappede typer
Den virkelige kraft i betingede og mappede typer kommer, når du kombinerer dem. Dette giver dig mulighed for at skabe meget fleksible og udtryksfulde typedefinitioner, der kan tilpasse sig en bred vifte af scenarier.Eksempel: Filtrering af egenskaber efter type
Lad os sige, du vil oprette en type, der filtrerer egenskaberne i et objekt baseret på deres type. For eksempel vil du måske kun udtrække strengegenskaberne fra et objekt.
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>;
// Resultat:
// {
// name: string;
// city: string;
// country: string;
// }
const stringData: StringData = {
name: "John",
city: "New York",
country: "USA",
};
I dette eksempel bruger StringProperties<T>
-typen en mappet type med nøgle-ommærkning og en betinget type. For hver egenskab K
kontrollerer den, om typen af T[K]
er en streng. Hvis den er det, bevares nøglen; ellers mappes den til never
, hvilket effektivt filtrerer den fra. never
som en mappet type-nøgle fjerner den fra den resulterende type. Dette sikrer, at kun strengegenskaber inkluderes i StringData
-typen.
Utility-typer i TypeScript
TypeScript leverer flere indbyggede utility-typer, der udnytter betingede og mappede typer til at udføre almindelige typetransformationer. At forstå disse utility-typer kan betydeligt forenkle din kode og forbedre typesikkerheden.
Almindelige utility-typer
Partial<T>
: Gør alle egenskaber iT
valgfrie.Readonly<T>
: Gør alle egenskaber iT
skrivebeskyttede.Required<T>
: Gør alle egenskaber iT
påkrævede. (fjerner?
-modifikatoren)Pick<T, K extends keyof T>
: Vælger et sæt egenskaberK
fraT
.Omit<T, K extends keyof T>
: Fjerner et sæt egenskaberK
fraT
.Record<K extends keyof any, T>
: Konstruerer en type med et sæt egenskaberK
af typenT
.Exclude<T, U>
: Udelukker alle typer fraT
, der kan tildeles tilU
.Extract<T, U>
: Udtrækker alle typer fraT
, der kan tildeles tilU
.NonNullable<T>
: Udelukkernull
ogundefined
fraT
.Parameters<T>
: Henter parametrene for en funktionstypeT
i en tuple.ReturnType<T>
: Henter returtypen for en funktionstypeT
.InstanceType<T>
: Henter instanstypen for en konstruktørfunktionstypeT
.ThisType<T>
: Fungerer som en markør for kontekstuelthis
-type.
Disse utility-typer er bygget ved hjælp af betingede og mappede typer, hvilket demonstrerer kraften og fleksibiliteten i disse avancerede TypeScript-funktioner. For eksempel er Partial<T>
defineret som:
type Partial<T> = {
[P in keyof T]?: T[P];
};
Bedste praksis for brug af betingede og mappede typer
Selvom betingede og mappede typer er kraftfulde, kan de også gøre din kode mere kompleks, hvis de ikke bruges omhyggeligt. Her er nogle bedste praksis, du skal huske på:
- Hold det enkelt: Undgå alt for komplekse betingede og mappede typer. Hvis en typedefinition bliver for indviklet, kan du overveje at opdele den i mindre, mere håndterbare dele.
- Brug meningsfulde navne: Giv dine betingede og mappede typer beskrivende navne, der tydeligt angiver deres formål.
- Dokumenter dine typer: Tilføj kommentarer for at forklare logikken bag dine betingede og mappede typer, især hvis de er komplekse.
- Udnyt utility-typer: Før du opretter en brugerdefineret betinget eller mappet type, skal du kontrollere, om en indbygget utility-type kan opnå det samme resultat.
- Test dine typer: Sørg for, at dine betingede og mappede typer opfører sig som forventet ved at skrive enhedstests, der dækker forskellige scenarier.
- Overvej ydeevne: Komplekse typeberegninger kan påvirke kompileringstider. Vær opmærksom på ydeevnekonsekvenserne af dine typedefinitioner.
Konklusion
Betingede og mappede typer er essentielle værktøjer til at mestre TypeScript. De giver dig mulighed for at skabe meget fleksible, typesikre og vedligeholdelsesvenlige applikationer, der tilpasser sig komplekse datastrukturer og dynamiske krav. Ved at forstå og anvende de koncepter, der er diskuteret i denne guide, kan du frigøre det fulde potentiale i TypeScript og skrive mere robust og skalerbar kode. Mens du fortsætter med at udforske TypeScript, skal du huske at eksperimentere med forskellige kombinationer af betingede og mappede typer for at opdage nye måder at løse udfordrende typeproblemer på. Mulighederne er virkelig uendelige.