En omfattende guide til TypeScript's kraftfulde Mapped Types og Conditional Types, inklusiv praktiske eksempler og avancerede anvendelser til at skabe robuste og typesikre applikationer.
Mestring af TypeScript's Mapped Types og Conditional Types
TypeScript, et supersæt af JavaScript, tilbyder kraftfulde funktioner til at skabe robuste og vedligeholdelsesvenlige applikationer. Blandt disse funktioner skiller Mapped Types og Conditional Types sig ud som essentielle værktøjer til avanceret typemanipulation. Denne guide giver en omfattende oversigt over disse koncepter, udforsker deres syntaks, praktiske anvendelser og avancerede use cases. Uanset om du er en erfaren TypeScript-udvikler eller lige er startet på din rejse, vil denne artikel udstyre dig med den viden, der skal til for at udnytte disse funktioner effektivt.
Hvad er Mapped Types?
Mapped Types giver dig mulighed for at oprette nye typer ved at transformere eksisterende. De itererer over egenskaberne i en eksisterende type og anvender en transformation på hver egenskab. Dette er især nyttigt til at skabe variationer af eksisterende typer, såsom at gøre alle egenskaber valgfri eller skrivebeskyttede.
Grundlæggende syntaks
Syntaksen for en Mapped Type er som følger:
type NewType<T> = {
[K in keyof T]: Transformation;
};
T
: Input-typen, du vil mappe over.K in keyof T
: Itererer over hver nøgle i input-typenT
.keyof T
skaber en union af alle egenskabsnavne iT
, ogK
repræsenterer hver enkelt nøgle under iterationen.Transformation
: Den transformation, du vil anvende på hver egenskab. Dette kan være at tilføje en modifikator (somreadonly
eller?
), ændre typen eller noget helt tredje.
Praktiske eksempler
Gør egenskaber skrivebeskyttede
Lad os sige, du har en interface, der repræsenterer en brugerprofil:
interface UserProfile {
name: string;
age: number;
email: string;
}
Du kan oprette en ny type, hvor alle egenskaber er skrivebeskyttede:
type ReadOnlyUserProfile = {
readonly [K in keyof UserProfile]: UserProfile[K];
};
Nu vil ReadOnlyUserProfile
have de samme egenskaber som UserProfile
, men de vil alle være skrivebeskyttede.
Gør egenskaber valgfri
På samme måde kan du gøre alle egenskaber valgfri:
type OptionalUserProfile = {
[K in keyof UserProfile]?: UserProfile[K];
};
OptionalUserProfile
vil have alle egenskaber fra UserProfile
, men hver egenskab vil være valgfri.
Modificering af egenskabstyper
Du kan også modificere typen for hver egenskab. For eksempel kan du transformere alle egenskaber til at være strenge:
type StringifiedUserProfile = {
[K in keyof UserProfile]: string;
};
I dette tilfælde vil alle egenskaber i StringifiedUserProfile
være af typen string
.
Hvad er Conditional Types?
Conditional Types giver dig mulighed for at definere typer, der afhænger af en betingelse. De giver en måde at udtrykke type-relationer på, baseret på om en type opfylder en bestemt begrænsning. Dette ligner en ternær operator i JavaScript, men for typer.
Grundlæggende syntaks
Syntaksen for en Conditional Type er som følger:
T extends U ? X : Y
T
: Typen der bliver tjekket.U
: Typen somT
udvider (betingelsen).X
: Typen der returneres, hvisT
udviderU
(betingelsen er sand).Y
: Typen der returneres, hvisT
ikke udviderU
(betingelsen er falsk).
Praktiske eksempler
Afgør om en type er en streng
Lad os oprette en type, der returnerer string
, hvis input-typen er en streng, og number
ellers:
type StringOrNumber<T> = T extends string ? string : number;
type Result1 = StringOrNumber<string>; // string
type Result2 = StringOrNumber<number>; // number
type Result3 = StringOrNumber<boolean>; // number
Udtrækning af type fra en Union
Du kan bruge conditional types til at udtrække en specifik type fra en union-type. For eksempel, for at udtrække ikke-nullable typer:
type NonNullable<T> = T extends null | undefined ? never : T;
type Result4 = NonNullable<string | null | undefined>; // string
Her, hvis T
er null
eller undefined
, bliver typen til never
, som derefter filtreres fra af TypeScript's forenkling af union-typer.
Inferring af Typer
Conditional types kan også bruges til at udlede (infer) typer ved hjælp af infer
nøgleordet. Dette giver dig mulighed for at udtrække en type fra en mere kompleks typestruktur.
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
I dette eksempel udtrækker ReturnType
returtypen fra en funktion. Den tjekker, om T
er en funktion, der tager vilkårlige argumenter og returnerer en type R
. Hvis den er det, returnerer den R
; ellers returnerer den any
.
Kombination af Mapped Types og Conditional Types
Den virkelige styrke ved Mapped Types og Conditional Types kommer fra at kombinere dem. Dette giver dig mulighed for at skabe meget fleksible og udtryksfulde type-transformationer.
Eksempel: Deep Readonly
En almindelig anvendelse er at skabe en type, der gør alle egenskaber i et objekt, inklusiv indlejrede egenskaber, skrivebeskyttede. Dette kan opnås ved hjælp af en rekursiv 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>;
Her anvender DeepReadonly
rekursivt readonly
modifikatoren på alle egenskaber og deres indlejrede egenskaber. Hvis en egenskab er et objekt, kalder den rekursivt DeepReadonly
på det objekt. Ellers anvender den blot readonly
modifikatoren på egenskaben.
Eksempel: Filtrering af egenskaber efter type
Lad os sige, at du vil oprette en type, der kun inkluderer egenskaber af en bestemt type. Du kan kombinere Mapped Types og Conditional Types for at opnå dette.
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>;
I dette eksempel itererer FilterByType
over egenskaberne i T
og tjekker, om typen af hver egenskab udvider U
. Hvis den gør det, inkluderer den egenskaben i den resulterende type; ellers ekskluderer den den ved at mappe nøglen til never
. Bemærk brugen af "as" til at remappe nøgler. Vi bruger derefter `Omit` og `keyof StringProperties` til at fjerne streng-egenskaberne fra den oprindelige interface.
Avancerede anvendelser og mønstre
Ud over de grundlæggende eksempler kan Mapped Types og Conditional Types bruges i mere avancerede scenarier til at skabe meget tilpassede og typesikre applikationer.
Distributive Conditional Types
Conditional types er distributive, når den type, der kontrolleres, er en union-type. Det betyder, at betingelsen anvendes på hvert medlem af unionen individuelt, og resultaterne kombineres derefter til en ny union-type.
type ToArray<T> = T extends any ? T[] : never;
type Result6 = ToArray<string | number>; // string[] | number[]
I dette eksempel anvendes ToArray
på hvert medlem af unionen string | number
individuelt, hvilket resulterer i string[] | number[]
. Hvis betingelsen ikke var distributiv, ville resultatet have været (string | number)[]
.
Brug af Utility Types
TypeScript tilbyder adskillige indbyggede utility-typer, der udnytter Mapped Types og Conditional Types. Disse utility-typer kan bruges som byggeklodser til mere komplekse type-transformationer.
Partial<T>
: Gør alle egenskaber iT
valgfrie.Required<T>
: Gør alle egenskaber iT
påkrævede.Readonly<T>
: Gør alle egenskaber iT
skrivebeskyttede.Pick<T, K>
: Vælger et sæt egenskaberK
fraT
.Omit<T, K>
: Fjerner et sæt egenskaberK
fraT
.Record<K, T>
: Konstruerer en type med et sæt egenskaberK
af typenT
.Exclude<T, U>
: Ekskluderer alle typer fraT
, der kan tildelesU
.Extract<T, U>
: Ekstraherer alle typer fraT
, der kan tildelesU
.NonNullable<T>
: Ekskluderernull
ogundefined
fraT
.Parameters<T>
: Henter parametrene for en funktionstypeT
.ReturnType<T>
: Henter returtypen for en funktionstypeT
.InstanceType<T>
: Henter instanstypen for en constructor-funktionstypeT
.
Disse utility-typer er kraftfulde værktøjer, der kan forenkle komplekse typemanipulationer. For eksempel kan du kombinere Pick
og Partial
for at skabe en type, der kun gør visse egenskaber valgfrie:
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">;
I dette eksempel har OptionalDescriptionProduct
alle egenskaberne fra Product
, men egenskaben description
er valgfri.
Brug af Template Literal Types
Template Literal Types giver dig mulighed for at oprette typer baseret på streng-literaler. De kan bruges i kombination med Mapped Types og Conditional Types til at skabe dynamiske og udtryksfulde type-transformationer. For eksempel kan du oprette en type, der præfikser alle egenskabsnavne med en bestemt streng:
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_">;
I dette eksempel vil PrefixedSettings
have egenskaberne data_apiUrl
og data_timeout
.
Bedste praksis og overvejelser
- Hold det simpelt: Selvom Mapped Types og Conditional Types er kraftfulde, kan de også gøre din kode mere kompleks. Prøv at holde dine type-transformationer så enkle som muligt.
- Brug Utility Types: Udnyt TypeScript's indbyggede utility-typer, når det er muligt. De er velafprøvede og kan forenkle din kode.
- Dokumenter dine typer: Dokumenter tydeligt dine type-transformationer, især hvis de er komplekse. Dette vil hjælpe andre udviklere med at forstå din kode.
- Test dine typer: Brug TypeScript's typekontrol til at sikre, at dine type-transformationer fungerer som forventet. Du kan skrive enhedstests for at verificere dine typers adfærd.
- Overvej ydeevne: Komplekse type-transformationer kan påvirke ydeevnen af din TypeScript-compiler. Vær opmærksom på kompleksiteten af dine typer og undgå unødvendige beregninger.
Konklusion
Mapped Types og Conditional Types er kraftfulde funktioner i TypeScript, der giver dig mulighed for at skabe meget fleksible og udtryksfulde type-transformationer. Ved at mestre disse koncepter kan du forbedre typesikkerheden, vedligeholdelsen og den overordnede kvalitet af dine TypeScript-applikationer. Fra simple transformationer som at gøre egenskaber valgfrie eller skrivebeskyttede til komplekse rekursive transformationer og betinget logik, giver disse funktioner de værktøjer, du har brug for til at bygge robuste og skalerbare applikationer. Fortsæt med at udforske og eksperimentere med disse funktioner for at frigøre deres fulde potentiale og blive en dygtigere TypeScript-udvikler.
Når du fortsætter din TypeScript-rejse, så husk at udnytte den rigdom af ressourcer, der er tilgængelige, herunder den officielle TypeScript-dokumentation, online fællesskaber og open source-projekter. Omfavn kraften i Mapped Types og Conditional Types, og du vil være godt rustet til at tackle selv de mest udfordrende type-relaterede problemer.