Frigjør kraften i TypeScript med avanserte betingede og mappede typer. Lær å lage fleksible, typesikre applikasjoner som tilpasser seg komplekse datastrukturer. Mestre kunsten å skrive ekte dynamisk TypeScript-kode.
Avanserte TypeScript-mønstre: Mestring av betingede og mappede typer
Styrken til TypeScript ligger i evnen til å tilby sterk typing, noe som lar deg fange feil tidlig og skrive mer vedlikeholdbar kode. Mens grunnleggende typer som string
, number
og boolean
er fundamentale, låser TypeScript sine avanserte funksjoner som betingede og mappede typer opp en ny dimensjon av fleksibilitet og typesikkerhet. Denne omfattende guiden vil dykke ned i disse kraftfulle konseptene og utstyre deg med kunnskapen til å lage ekte dynamiske og tilpasningsdyktige TypeScript-applikasjoner.
Hva er betingede typer?
Betingede typer lar deg definere typer som avhenger av en betingelse, likt en ternær operator i JavaScript (condition ? trueValue : falseValue
). De gjør det mulig å uttrykke komplekse typerelasjoner basert på om en type tilfredsstiller en spesifikk begrensning.
Syntaks
Den grunnleggende syntaksen for en betinget type er:
T extends U ? X : Y
T
: Typen som sjekkes.U
: Typen det sjekkes mot.extends
: Nøkkelordet som indikerer et subtyperelasjon.X
: Typen som brukes hvisT
kan tilordnesU
.Y
: Typen som brukes hvisT
ikke kan tilordnesU
.
I hovedsak, hvis T extends U
evalueres til sant, løses typen til X
; ellers løses den til Y
.
Praktiske eksempler
1. Bestemme typen til en funksjonsparameter
La oss si at du vil lage en type som bestemmer om en funksjonsparameter er en streng eller et tall:
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"); // Utdata: Value is a string: hello
processValue(123); // Utdata: Value is a number: 123
I dette eksempelet er ParamType<T>
en betinget type. Hvis T
er en streng, løses typen til string
; ellers løses den til number
. Funksjonen processValue
godtar enten en streng eller et tall basert på denne betingede typen.
2. Hente ut returtype basert på inndatatype
Se for deg et scenario der du har en funksjon som returnerer forskjellige typer basert på inndataene. Betingede typer kan hjelpe deg med å definere den korrekte 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")); // Utdata: 7
console.log(numberProcessor.process(42)); // Utdata: "42"
Her velger typen Processor<T>
betinget enten StringProcessor
eller NumberProcessor
basert på typen av inndataene. Dette sikrer at funksjonen createProcessor
returnerer riktig type prosessorobjekt.
3. Diskriminerte unioner
Betingede typer er ekstremt kraftige når man jobber med diskriminerte unioner. En diskriminert union er en unionstype der hvert medlem har en felles, singleton-type egenskap (diskriminanten). Dette lar deg snevre inn typen basert på verdien 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)); // Utdata: 25
console.log(calculateArea(myCircle)); // Utdata: 28.274333882308138
I dette eksempelet er Shape
-typen en diskriminert union. Typen Area<T>
bruker en betinget type for å bestemme om formen er en firkant eller en sirkel, og returnerer en number
for firkanter og en string
for sirkler (selv om du i et virkelig scenario sannsynligvis ville ønsket konsistente returtyper, demonstrerer dette prinsippet).
Viktige poenger om betingede typer
- Gjør det mulig å definere typer basert på betingelser.
- Forbedrer typesikkerheten ved å uttrykke komplekse typerelasjoner.
- Er nyttige for å jobbe med funksjonsparametere, returtyper og diskriminerte unioner.
Hva er mappede typer?
Mappede typer gir en måte å transformere eksisterende typer på ved å mappe over deres egenskaper. De lar deg lage nye typer basert på egenskapene til en annen type, ved å anvende modifikasjoner som å gjøre egenskaper valgfrie, skrivebeskyttede eller endre typene deres.
Syntaks
Den generelle syntaksen for en mappet type er:
type NewType<T> = {
[K in keyof T]: ModifiedType;
};
T
: Inndatatypen.keyof T
: En typeoperator som returnerer en union av alle egenskapsnøkler iT
.K in keyof T
: Itererer over hver nøkkel ikeyof T
, og tilordner hver nøkkel til typevariabelenK
.ModifiedType
: Typen som hver egenskap vil bli mappet til. Dette kan inkludere betingede typer eller andre typetransformasjoner.
Praktiske eksempler
1. Gjøre egenskaper valgfrie
Du kan bruke en mappet type for å gjøre alle egenskapene til 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",
}; // Gyldig, siden 'id' og 'email' er valgfrie
Her er PartialUser
en mappet type som itererer over nøklene i User
-grensesnittet. For hver nøkkel K
, gjør den egenskapen valgfri ved å legge til ?
-modifikatoren. User[K]
henter typen til egenskapen K
fra User
-grensesnittet.
2. Gjøre egenskaper skrivebeskyttet
På samme måte kan du gjøre alle egenskapene til en eksisterende type skrivebeskyttet:
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; // Feil: Kan ikke tilordne til 'price' fordi det er en skrivebeskyttet egenskap.
I dette tilfellet er ReadonlyProduct
en mappet type som legger til readonly
-modifikatoren til hver egenskap i Product
-grensesnittet.
3. Transformere egenskapstyper
Mappede typer kan også brukes til å transformere typene til egenskaper. For eksempel kan du lage en type som konverterer alle strengegenskaper til tall:
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å være et tall på grunn av mappingen
timeout: 456, // Må være et tall på grunn av mappingen
maxRetries: 3,
};
Dette eksempelet demonstrerer bruk av en betinget type innenfor en mappet type. For hver egenskap K
, sjekker den om typen til Config[K]
er en streng. Hvis den er det, mappes typen til number
; ellers forblir den uendret.
4. Nøkkel-remapping (siden TypeScript 4.1)
TypeScript 4.1 introduserte muligheten til å remap-e nøkler innenfor mappede typer ved hjelp av as
-nøkkelordet. Dette lar deg lage nye typer med forskjellige egenskapsnavn basert på den opprinnelige 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-funksjon brukt for å gjøre første bokstav stor
type Capitalize<S extends string> = Uppercase<string&S> extends never ? string : `$Capitalize<S>`;
//Bruk med et faktisk objekt
const myEvent: TransformedEvent = {
newEventId: "123",
newEventName: "New Name",
newEventDate: new Date()
};
Her remap-er TransformedEvent
-typen hver nøkkel K
til en ny nøkkel med prefikset "new" og stor forbokstav. Verktøyfunksjonen `Capitalize`, sikrer at den første bokstaven i nøkkelen blir stor. Snittet `string & K` sikrer at vi kun håndterer strengnøkler og at vi får den korrekte literaltypen fra K.
Nøkkel-remapping åpner for kraftige muligheter for å transformere og tilpasse typer til spesifikke behov. Dette lar deg gi nytt navn til, filtrere eller modifisere nøkler basert på kompleks logikk.
Viktige poenger om mappede typer
- Gjør det mulig å transformere eksisterende typer ved å mappe over deres egenskaper.
- Lar deg gjøre egenskaper valgfrie, skrivebeskyttede eller endre typene deres.
- Er nyttige for å lage nye typer basert på egenskapene til en annen type.
- Nøkkel-remapping (introdusert i TypeScript 4.1) tilbyr enda større fleksibilitet i typetransformasjoner.
Kombinere betingede og mappede typer
Den virkelige kraften til betingede og mappede typer kommer når du kombinerer dem. Dette lar deg lage svært fleksible og uttrykksfulle typedefinisjoner som kan tilpasse seg et bredt spekter av scenarier.Eksempel: Filtrere egenskaper etter type
La oss si at du vil lage en type som filtrerer egenskapene til et objekt basert på deres type. For eksempel kan du ønske å hente ut bare strengegenskapene 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 eksempelet bruker StringProperties<T>
-typen en mappet type med nøkkel-remapping og en betinget type. For hver egenskap K
, sjekker den om typen til T[K]
er en streng. Hvis den er det, beholdes nøkkelen; ellers mappes den til never
, noe som effektivt filtrerer den ut. never
som en mappet typenøkkel fjerner den fra den resulterende typen. Dette sikrer at bare strengegenskaper inkluderes i StringData
-typen.
Verktøytyper i TypeScript
TypeScript tilbyr flere innebygde verktøytyper som utnytter betingede og mappede typer for å utføre vanlige typetransformasjoner. Å forstå disse verktøytypene kan betydelig forenkle koden din og forbedre typesikkerheten.
Vanlige verktøytyper
Partial<T>
: Gjør alle egenskapene tilT
valgfrie.Readonly<T>
: Gjør alle egenskapene tilT
skrivebeskyttede.Required<T>
: Gjør alle egenskapene tilT
påkrevde. (fjerner?
-modifikatoren)Pick<T, K extends keyof T>
: Velger et sett med egenskaperK
fraT
.Omit<T, K extends keyof T>
: Fjerner et sett med egenskaperK
fraT
.Record<K extends keyof any, T>
: Konstruerer en type med et sett med egenskaperK
av typenT
.Exclude<T, U>
: Ekskluderer fraT
alle typer som kan tilordnesU
.Extract<T, U>
: Henter ut fraT
alle typer som kan tilordnesU
.NonNullable<T>
: Ekskluderernull
ogundefined
fraT
.Parameters<T>
: Henter parameterne til en funksjonstypeT
i en tuppel.ReturnType<T>
: Henter returtypen til en funksjonstypeT
.InstanceType<T>
: Henter instanstypen til en konstruktørfunksjonstypeT
.ThisType<T>
: Fungerer som en markør for kontekstuellthis
-type.
Disse verktøytypene er bygget ved hjelp av betingede og mappede typer, noe som demonstrerer kraften og fleksibiliteten til disse avanserte TypeScript-funksjonene. For eksempel er Partial<T>
definert som:
type Partial<T> = {
[P in keyof T]?: T[P];
};
Beste praksis for bruk av betingede og mappede typer
Selv om betingede og mappede typer er kraftige, kan de også gjøre koden din mer kompleks hvis de ikke brukes forsiktig. Her er noen beste praksiser å huske på:
- Hold det enkelt: Unngå altfor komplekse betingede og mappede typer. Hvis en typedefinisjon blir for innviklet, bør du vurdere å bryte den ned i mindre, mer håndterbare deler.
- Bruk meningsfulle navn: Gi dine betingede og mappede typer beskrivende navn som tydelig indikerer formålet deres.
- Dokumenter typene dine: Legg til kommentarer for å forklare logikken bak dine betingede og mappede typer, spesielt hvis de er komplekse.
- Utnytt verktøytyper: Før du lager en tilpasset betinget eller mappet type, sjekk om en innebygd verktøytype kan oppnå samme resultat.
- Test typene dine: Sørg for at dine betingede og mappede typer oppfører seg som forventet ved å skrive enhetstester som dekker forskjellige scenarier.
- Vurder ytelse: Komplekse typeberegninger kan påvirke kompileringstiden. Vær oppmerksom på ytelseskonsekvensene av typedefinisjonene dine.
Konklusjon
Betingede og mappede typer er essensielle verktøy for å mestre TypeScript. De gjør det mulig å lage svært fleksible, typesikre og vedlikeholdbare applikasjoner som tilpasser seg komplekse datastrukturer og dynamiske krav. Ved å forstå og anvende konseptene som er diskutert i denne guiden, kan du frigjøre det fulle potensialet til TypeScript og skrive mer robust og skalerbar kode. Mens du fortsetter å utforske TypeScript, husk å eksperimentere med forskjellige kombinasjoner av betingede og mappede typer for å oppdage nye måter å løse utfordrende typeproblemer på. Mulighetene er virkelig uendelige.