LÄs upp kraften i TypeScript med avancerade villkorliga och mappade typer. LÀr dig skapa flexibla, typsÀkra applikationer som anpassar sig till komplexa datastrukturer. BemÀstra konsten att skriva verkligt dynamisk TypeScript-kod.
Avancerade TypeScript-mönster: BemÀstra villkorliga och mappade typer
Styrkan med TypeScript ligger i dess förmÄga att tillhandahÄlla stark typning, vilket gör att du kan fÄnga fel tidigt och skriva mer underhÄllbar kod. Medan grundlÀggande typer som string
, number
och boolean
Àr fundamentala, lÄser TypeScript's avancerade funktioner som villkorliga och mappade typer upp en ny dimension av flexibilitet och typsÀkerhet. Denna omfattande guide kommer att djupdyka i dessa kraftfulla koncept och utrusta dig med kunskapen för att skapa verkligt dynamiska och anpassningsbara TypeScript-applikationer.
Vad Àr villkorliga typer?
Villkorliga typer lÄter dig definiera typer som beror pÄ ett villkor, liknande en ternÀr operator i JavaScript (condition ? trueValue : falseValue
). De gör det möjligt för dig att uttrycka komplexa typrelationer baserat pÄ om en typ uppfyller ett specifikt villkor.
Syntax
Den grundlÀggande syntaxen för en villkorlig typ Àr:
T extends U ? X : Y
T
: Typen som kontrolleras.U
: Typen att kontrollera mot.extends
: Nyckelordet som indikerar en subtypsrelation.X
: Typen som anvÀnds omT
Ă€r tilldelningsbar tillU
.Y
: Typen som anvÀnds omT
inte Àr tilldelningsbar tillU
.
I grund och botten, om T extends U
utvÀrderas till sant, löses typen till X
; annars löses den till Y
.
Praktiska exempel
1. BestÀmma typen pÄ en funktionsparameter
LÄt oss sÀga att du vill skapa en typ som avgör om en funktionsparameter Àr en strÀng eller ett nummer:
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
I detta exempel Àr ParamType<T>
en villkorlig typ. Om T
Àr en strÀng, löses typen till string
; annars löses den till number
. Funktionen processValue
accepterar antingen en strÀng eller ett nummer baserat pÄ denna villkorliga typ.
2. Extrahera returtyp baserat pÄ indatatyp
FörestÀll dig ett scenario dÀr du har en funktion som returnerar olika typer baserat pÄ indata. Villkorliga typer kan hjÀlpa dig att definiera den korrekta returtypen:
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"
HÀr vÀljer Processor<T>
-typen villkorligt antingen StringProcessor
eller NumberProcessor
baserat pÄ typen av indata. Detta sÀkerstÀller att createProcessor
-funktionen returnerar rÀtt typ av processorobjekt.
3. Diskriminerade unioner
Villkorliga typer Àr extremt kraftfulla nÀr man arbetar med diskriminerade unioner. En diskriminerad union Àr en unionstyp dÀr varje medlem har en gemensam, singleton-typegenskap (diskriminanten). Detta gör att du kan avgrÀnsa typen baserat pÄ vÀrdet av den egenskapen.
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 detta exempel Àr Shape
-typen en diskriminerad union. Area<T>
-typen anvÀnder en villkorlig typ för att avgöra om formen Àr en kvadrat eller en cirkel, och returnerar en number
för kvadrater och en string
för cirklar (Àven om man i ett verkligt scenario troligen skulle vilja ha konsekventa returtyper, visar detta principen).
Viktiga lÀrdomar om villkorliga typer
- Möjliggör definition av typer baserat pÄ villkor.
- FörbÀttrar typsÀkerheten genom att uttrycka komplexa typrelationer.
- Ăr anvĂ€ndbara för att arbeta med funktionsparametrar, returtyper och diskriminerade unioner.
Vad Àr mappade typer?
Mappade typer erbjuder ett sÀtt att omvandla befintliga typer genom att mappa över deras egenskaper. De lÄter dig skapa nya typer baserade pÄ egenskaperna hos en annan typ, och tillÀmpa modifieringar som att göra egenskaper valfria, skrivskyddade eller Àndra deras typer.
Syntax
Den allmÀnna syntaxen för en mappad typ Àr:
type NewType<T> = {
[K in keyof T]: ModifiedType;
};
T
: Indatatypen.keyof T
: En typoperator som returnerar en union av alla egenskapsnycklar iT
.K in keyof T
: Itererar över varje nyckel ikeyof T
, och tilldelar varje nyckel till typvariabelnK
.ModifiedType
: Typen som varje egenskap kommer att mappas till. Detta kan inkludera villkorliga typer eller andra typomvandlingar.
Praktiska exempel
1. Göra egenskaper valfria
Du kan anvÀnda en mappad typ för att göra alla egenskaper i en befintlig typ valfria:
interface User {
id: number;
name: string;
email: string;
}
type PartialUser = {
[K in keyof User]?: User[K];
};
const partialUser: PartialUser = {
name: "John Doe",
}; // Giltigt, eftersom 'id' och 'email' Àr valfria
HÀr Àr PartialUser
en mappad typ som itererar över nycklarna i User
-interfacet. För varje nyckel K
gör den egenskapen valfri genom att lÀgga till ?
-modifieraren. User[K]
hÀmtar typen för egenskapen K
frÄn User
-interfacet.
2. Göra egenskaper skrivskyddade
PÄ liknande sÀtt kan du göra alla egenskaper i en befintlig typ skrivskyddade:
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; // Fel: Kan inte tilldela till 'price' eftersom det Àr en skrivskyddad egenskap.
I detta fall Àr ReadonlyProduct
en mappad typ som lÀgger till readonly
-modifieraren till varje egenskap i Product
-interfacet.
3. Omvandla egenskapstyper
Mappade typer kan ocksÄ anvÀndas för att omvandla egenskapernas typer. Du kan till exempel skapa en typ som konverterar alla strÀngegenskaper till nummer:
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, // MÄste vara ett nummer pÄ grund av mappningen
timeout: 456, // MÄste vara ett nummer pÄ grund av mappningen
maxRetries: 3,
};
Detta exempel demonstrerar anvÀndningen av en villkorlig typ inuti en mappad typ. För varje egenskap K
kontrollerar den om typen av Config[K]
Àr en strÀng. Om den Àr det, mappas typen till number
; annars förblir den oförÀndrad.
4. Ommöblering av nycklar (sedan TypeScript 4.1)
TypeScript 4.1 introducerade möjligheten att möblera om nycklar inom mappade typer med hjÀlp av nyckelordet as
. Detta lÄter dig skapa nya typer med olika egenskapsnamn baserat pÄ den ursprungliga typen.
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 som anvÀnds för att göra första bokstaven stor
type Capitalize<S extends string> = Uppercase<string&S> extends never ? string : `$Capitalize<S>`;
//AnvÀndning med ett faktiskt objekt
const myEvent: TransformedEvent = {
newEventId: "123",
newEventName: "New Name",
newEventDate: new Date()
};
HÀr möblerar TransformedEvent
-typen om varje nyckel K
till en ny nyckel med prefixet "new" och stor första bokstav. `Capitalize`-hjÀlpfunktionen sÀkerstÀller att den första bokstaven i nyckeln blir stor. `string & K`-intersectionen sÀkerstÀller att vi bara hanterar strÀngnycklar och att vi fÄr rÀtt bokstavliga typ frÄn K. Ommöblering av nycklar öppnar upp kraftfulla möjligheter för att omvandla och anpassa typer till specifika behov. Detta gör att du kan byta namn pÄ, filtrera eller modifiera nycklar baserat pÄ komplex logik.
Viktiga lÀrdomar om mappade typer
- Möjliggör omvandling av befintliga typer genom att mappa över deras egenskaper.
- TillÄter att göra egenskaper valfria, skrivskyddade eller Àndra deras typer.
- Ăr anvĂ€ndbara för att skapa nya typer baserade pĂ„ egenskaperna hos en annan typ.
- Ommöblering av nycklar (introducerat i TypeScript 4.1) erbjuder Ànnu större flexibilitet i typomvandlingar.
Kombinera villkorliga och mappade typer
Den verkliga kraften hos villkorliga och mappade typer kommer nÀr du kombinerar dem. Detta lÄter dig skapa mycket flexibla och uttrycksfulla typdefinitioner som kan anpassas till ett brett spektrum av scenarier.Exempel: Filtrera egenskaper efter typ
LÄt oss sÀga att du vill skapa en typ som filtrerar egenskaperna hos ett objekt baserat pÄ deras typ. Du kanske till exempel vill extrahera endast strÀngegenskaperna frÄn ett 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 detta exempel anvÀnder StringProperties<T>
-typen en mappad typ med ommöblering av nycklar och en villkorlig typ. För varje egenskap K
kontrollerar den om typen av T[K]
Àr en strÀng. Om den Àr det, behÄlls nyckeln; annars mappas den till never
, vilket effektivt filtrerar bort den. never
som en mappad typnyckel tar bort den frÄn den resulterande typen. Detta sÀkerstÀller att endast strÀngegenskaper inkluderas i StringData
-typen.
HjÀlptyper (Utility Types) i TypeScript
TypeScript tillhandahÄller flera inbyggda hjÀlptyper (utility types) som utnyttjar villkorliga och mappade typer för att utföra vanliga typomvandlingar. Att förstÄ dessa hjÀlptyper kan avsevÀrt förenkla din kod och förbÀttra typsÀkerheten.
Vanliga hjÀlptyper
Partial<T>
: Gör alla egenskaper iT
valfria.Readonly<T>
: Gör alla egenskaper iT
skrivskyddade.Required<T>
: Gör alla egenskaper iT
obligatoriska. (tar bort?
-modifieraren)Pick<T, K extends keyof T>
: VÀljer en uppsÀttning egenskaperK
frÄnT
.Omit<T, K extends keyof T>
: Tar bort en uppsÀttning egenskaperK
frÄnT
.Record<K extends keyof any, 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
i en tuppel.ReturnType<T>
: HÀmtar returtypen för en funktionstypT
.InstanceType<T>
: HÀmtar instanstypen för en konstruktorfunktionstypT
.ThisType<T>
: Fungerar som en markör för kontextuellthis
-typ.
Dessa hjÀlptyper Àr byggda med villkorliga och mappade typer, vilket visar kraften och flexibiliteten hos dessa avancerade TypeScript-funktioner. Till exempel Àr Partial<T>
definierad som:
type Partial<T> = {
[P in keyof T]?: T[P];
};
BÀsta praxis för att anvÀnda villkorliga och mappade typer
Ăven om villkorliga och mappade typer Ă€r kraftfulla kan de ocksĂ„ göra din kod mer komplex om de inte anvĂ€nds varsamt. HĂ€r Ă€r nĂ„gra bĂ€sta praxis att ha i Ă„tanke:
- HÄll det enkelt: Undvik överdrivet komplexa villkorliga och mappade typer. Om en typdefinition blir för invecklad, övervÀg att bryta ner den i mindre, mer hanterbara delar.
- AnvÀnd meningsfulla namn: Ge dina villkorliga och mappade typer beskrivande namn som tydligt indikerar deras syfte.
- Dokumentera dina typer: LÀgg till kommentarer för att förklara logiken bakom dina villkorliga och mappade typer, sÀrskilt om de Àr komplexa.
- Utnyttja hjÀlptyper: Innan du skapar en anpassad villkorlig eller mappad typ, kontrollera om en inbyggd hjÀlptyp kan uppnÄ samma resultat.
- Testa dina typer: Se till att dina villkorliga och mappade typer beter sig som förvÀntat genom att skriva enhetstester som tÀcker olika scenarier.
- TÀnk pÄ prestanda: Komplexa typberÀkningar kan pÄverka kompileringstiderna. Var medveten om prestandaimplikationerna av dina typdefinitioner.
Slutsats
Villkorliga och mappade typer Àr vÀsentliga verktyg för att bemÀstra TypeScript. De gör det möjligt för dig att skapa mycket flexibla, typsÀkra och underhÄllbara applikationer som anpassar sig till komplexa datastrukturer och dynamiska krav. Genom att förstÄ och tillÀmpa de koncept som diskuteras i denna guide kan du lÄsa upp den fulla potentialen hos TypeScript och skriva mer robust och skalbar kod. NÀr du fortsÀtter att utforska TypeScript, kom ihÄg att experimentera med olika kombinationer av villkorliga och mappade typer för att upptÀcka nya sÀtt att lösa utmanande typproblem. Möjligheterna Àr verkligen oÀndliga.