En omfattande guide till TypeScript's kraftfulla Mapped Types och Conditional Types, med praktiska exempel och avancerade användningsfall för att skapa robusta och typsäkra applikationer.
Bemästra TypeScript's Mapped Types och Conditional Types
TypeScript, ett superset av JavaScript, erbjuder kraftfulla funktioner för att skapa robusta och underhållbara applikationer. Bland dessa funktioner utmärker sig Mapped Types och Conditional Types som väsentliga verktyg för avancerad typhandläggning. Denna guide ger en omfattande översikt av dessa koncept, utforskar deras syntax, praktiska tillämpningar och avancerade användningsfall. Oavsett om du är en erfaren TypeScript-utvecklare eller precis har börjat din resa, kommer den här artikeln att utrusta dig med kunskapen för att utnyttja dessa funktioner effektivt.
Vad är Mapped Types?
Mapped Types låter dig skapa nya typer genom att omvandla befintliga. De itererar över egenskaperna i en befintlig typ och applicerar en omvandling på varje egenskap. Detta är särskilt användbart för att skapa variationer av befintliga typer, som att göra alla egenskaper valfria eller skrivskyddade.
Grundläggande syntax
Syntaxen för en Mapped Type är som följer:
type NewType<T> = {
[K in keyof T]: Transformation;
};
T
: Indatatypen som du vill mappa över.K in keyof T
: Itererar över varje nyckel i indatatypenT
.keyof T
skapar en union av alla egenskapsnamn iT
, ochK
representerar varje enskild nyckel under iterationen.Transformation
: Omvandlingen du vill tillämpa på varje egenskap. Detta kan vara att lägga till en modifierare (somreadonly
eller?
), ändra typen eller något helt annat.
Praktiska exempel
Göra egenskaper skrivskyddade
Låt oss säga att du har ett interface som representerar en användarprofil:
interface UserProfile {
name: string;
age: number;
email: string;
}
Du kan skapa en ny typ där alla egenskaper är skrivskyddade:
type ReadOnlyUserProfile = {
readonly [K in keyof UserProfile]: UserProfile[K];
};
Nu kommer ReadOnlyUserProfile
att ha samma egenskaper som UserProfile
, men de kommer alla att vara skrivskyddade.
Göra egenskaper valfria
På samma sätt kan du göra alla egenskaper valfria:
type OptionalUserProfile = {
[K in keyof UserProfile]?: UserProfile[K];
};
OptionalUserProfile
kommer att ha alla egenskaper från UserProfile
, men varje egenskap kommer att vara valfri.
Modifiera egenskapstyper
Du kan också modifiera typen för varje egenskap. Till exempel kan du omvandla alla egenskaper till att vara strängar:
type StringifiedUserProfile = {
[K in keyof UserProfile]: string;
};
I det här fallet kommer alla egenskaper i StringifiedUserProfile
att vara av typen string
.
Vad är Conditional Types?
Conditional Types låter dig definiera typer som beror på ett villkor. De ger ett sätt att uttrycka typrelationer baserat på om en typ uppfyller ett visst krav. Detta liknar en ternär operator i JavaScript, men för typer.
Grundläggande syntax
Syntaxen för en Conditional Type är som följer:
T extends U ? X : Y
T
: Typen som kontrolleras.U
: Typen somT
utökar (villkoret).X
: Typen som ska returneras omT
extendsU
(villkoret är sant).Y
: Typen som ska returneras omT
inte extendsU
(villkoret är falskt).
Praktiska exempel
Avgöra om en typ är en sträng
Låt oss skapa en typ som returnerar string
om indatatypen är en sträng, och number
i annat fall:
type StringOrNumber<T> = T extends string ? string : number;
type Result1 = StringOrNumber<string>; // string
type Result2 = StringOrNumber<number>; // number
type Result3 = StringOrNumber<boolean>; // number
Extrahera typ från en union
Du kan använda villkorliga typer för att extrahera en specifik typ från en union-typ. Till exempel för att extrahera icke-nullbara typer:
type NonNullable<T> = T extends null | undefined ? never : T;
type Result4 = NonNullable<string | null | undefined>; // string
Här, om T
är null
eller undefined
, blir typen never
, vilket sedan filtreras bort av TypeScript's förenkling av union-typer.
Härleda typer (Inferring Types)
Villkorliga typer kan också användas för att härleda typer med nyckelordet infer
. Detta gör att du kan extrahera en typ från en mer komplex typstruktur.
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 det här exemplet extraherar ReturnType
returtypen från en funktion. Den kontrollerar om T
är en funktion som tar vilka argument som helst och returnerar en typ R
. Om den är det, returnerar den R
; annars returnerar den any
.
Kombinera Mapped Types och Conditional Types
Den verkliga kraften med Mapped Types och Conditional Types kommer från att kombinera dem. Detta gör att du kan skapa mycket flexibla och uttrycksfulla typomvandlingar.
Exempel: Deep Readonly
Ett vanligt användningsfall är att skapa en typ som gör alla egenskaper i ett objekt, inklusive nästlade egenskaper, skrivskyddade. Detta kan uppnås med en rekursiv villkorlig typ.
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>;
Här applicerar DeepReadonly
rekursivt readonly
-modifieraren på alla egenskaper och deras nästlade egenskaper. Om en egenskap är ett objekt anropar den rekursivt DeepReadonly
på det objektet. Annars applicerar den helt enkelt readonly
-modifieraren på egenskapen.
Exempel: Filtrera egenskaper efter typ
Låt oss säga att du vill skapa en typ som bara inkluderar egenskaper av en specifik typ. Du kan kombinera Mapped Types och Conditional Types för att uppnå detta.
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 detta exempel itererar FilterByType
över egenskaperna i T
och kontrollerar om typen för varje egenskap utökar U
. Om den gör det, inkluderas egenskapen i den resulterande typen; annars exkluderas den genom att mappa nyckeln till never
. Notera användningen av "as" för att mappa om nycklar. Vi använder sedan `Omit` och `keyof StringProperties` för att ta bort strängegenskaperna från det ursprungliga interfacet.
Avancerade användningsfall och mönster
Utöver de grundläggande exemplen kan Mapped Types och Conditional Types användas i mer avancerade scenarier för att skapa mycket anpassningsbara och typsäkra applikationer.
Distributiva villkorliga typer
Villkorliga typer är distributiva när typen som kontrolleras är en union-typ. Detta innebär att villkoret tillämpas på varje medlem i unionen individuellt, och resultaten kombineras sedan till en ny union-typ.
type ToArray<T> = T extends any ? T[] : never;
type Result6 = ToArray<string | number>; // string[] | number[]
I detta exempel tillämpas ToArray
på varje medlem i unionen string | number
individuellt, vilket resulterar i string[] | number[]
. Om villkoret inte hade varit distributivt skulle resultatet ha varit (string | number)[]
.
Använda Utility Types
TypeScript tillhandahåller flera inbyggda "utility types" som utnyttjar Mapped Types och Conditional Types. Dessa "utility types" kan användas som byggstenar för mer komplexa typomvandlingar.
Partial<T>
: Gör alla egenskaper iT
valfria.Required<T>
: Gör alla egenskaper iT
obligatoriska.Readonly<T>
: Gör alla egenskaper iT
skrivskyddade.Pick<T, K>
: Väljer en uppsättning egenskaperK
frånT
.Omit<T, K>
: Tar bort en uppsättning egenskaperK
frånT
.Record<K, T>
: Konstruerar en typ med en uppsättning egenskaperK
av typenT
.Exclude<T, U>
: Exkluderar frånT
alla typer som är tilldelningsbara tillU
.Extract<T, U>
: Extraherar frånT
alla typer som är tilldelningsbara tillU
.NonNullable<T>
: Exkluderarnull
ochundefined
frånT
.Parameters<T>
: Hämtar parametrarna för en funktionstypT
.ReturnType<T>
: Hämtar returtypen för en funktionstypT
.InstanceType<T>
: Hämtar instanstypen för en konstruktorfunktionstypT
.
Dessa "utility types" är kraftfulla verktyg som kan förenkla komplexa typhanteringar. Du kan till exempel kombinera Pick
och Partial
för att skapa en typ som gör endast vissa egenskaper valfria:
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 detta exempel har OptionalDescriptionProduct
alla egenskaper från Product
, men egenskapen description
är valfri.
Använda Template Literal Types
Template Literal Types låter dig skapa typer baserade på strängliteraler. De kan användas i kombination med Mapped Types och Conditional Types för att skapa dynamiska och uttrycksfulla typomvandlingar. Till exempel kan du skapa en typ som lägger till ett prefix till alla egenskapsnamn med en specifik sträng:
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 detta exempel kommer PrefixedSettings
att ha egenskaperna data_apiUrl
och data_timeout
.
Bästa praxis och överväganden
- Håll det enkelt: Även om Mapped Types och Conditional Types är kraftfulla kan de också göra din kod mer komplex. Försök att hålla dina typomvandlingar så enkla som möjligt.
- Använd Utility Types: Utnyttja TypeScript's inbyggda "utility types" när det är möjligt. De är vältestade och kan förenkla din kod.
- Dokumentera dina typer: Dokumentera tydligt dina typomvandlingar, särskilt om de är komplexa. Detta hjälper andra utvecklare att förstå din kod.
- Testa dina typer: Använd TypeScript's typkontroll för att säkerställa att dina typomvandlingar fungerar som förväntat. Du kan skriva enhetstester för att verifiera beteendet hos dina typer.
- Tänk på prestanda: Komplexa typomvandlingar kan påverka prestandan hos din TypeScript-kompilator. Var medveten om komplexiteten i dina typer och undvik onödiga beräkningar.
Slutsats
Mapped Types och Conditional Types är kraftfulla funktioner i TypeScript som gör det möjligt för dig att skapa mycket flexibla och uttrycksfulla typomvandlingar. Genom att bemästra dessa koncept kan du förbättra typsäkerheten, underhållbarheten och den övergripande kvaliteten på dina TypeScript-applikationer. Från enkla omvandlingar som att göra egenskaper valfria eller skrivskyddade till komplexa rekursiva omvandlingar och villkorlig logik, ger dessa funktioner de verktyg du behöver för att bygga robusta och skalbara applikationer. Fortsätt att utforska och experimentera med dessa funktioner för att låsa upp deras fulla potential och bli en mer skicklig TypeScript-utvecklare.
När du fortsätter din TypeScript-resa, kom ihåg att utnyttja den mängd resurser som finns tillgängliga, inklusive den officiella TypeScript-dokumentationen, online-communities och open source-projekt. Omfamna kraften i Mapped Types och Conditional Types, och du kommer att vara väl rustad för att ta itu med även de mest utmanande typrelaterade problemen.