Utforsk avanserte TypeScript-funksjoner som mal-literal- og betingede typer for å skrive mer uttrykksfull og vedlikeholdbar kode. Mestre type-manipulering for komplekse scenarioer.
TypeScript Avanserte Typer: Mestre Mal-Literal- og Betingede Typer
TypeScript sin styrke ligger i sitt kraftige typesystem. Mens grunnleggende typer som string, number og boolean er tilstrekkelige for mange scenarier, åpner avanserte funksjoner som mal-literal-typer og betingede typer opp for et nytt nivå av uttrykksfullhet og typesikkerhet. Denne guiden gir en omfattende oversikt over disse avanserte typene, utforsker deres muligheter og demonstrerer praktiske anvendelser.
Forstå Mal-Literal-Typer
Mal-literal-typer bygger på JavaScripts mal-literaler, og lar deg definere typer basert på streng-interpolering. Dette muliggjør opprettelse av typer som representerer spesifikke strengmønstre, noe som gjør koden din mer robust og forutsigbar.
Grunnleggende Syntaks og Bruk
Mal-literal-typer bruker backticks (`) for å omslutte type-definisjonen, likt JavaScript mal-literaler. Innenfor backtickene kan du interpolere andre typer ved å bruke ${}-syntaksen. Det er her magien skjer – du oppretter i hovedsak en type som er en streng, konstruert ved kompileringstid basert på typene inne i interpoleringen.
type HTTPMethod = "GET" | "POST" | "PUT" | "DELETE";
type APIEndpoint = `/api/${string}`;
// Eksempelbruk
const getEndpoint: APIEndpoint = "/api/users"; // Gyldig
const postEndpoint: APIEndpoint = "/api/products/123"; // Gyldig
const invalidEndpoint: APIEndpoint = "/admin/settings"; // TypeScript vil ikke vise en feil her da `string` kan være hva som helst
I dette eksempelet er APIEndpoint en type som representerer enhver streng som starter med /api/. Selv om dette grunnleggende eksempelet er nyttig, kommer den sanne kraften til mal-literal-typer frem når de kombineres med mer spesifikke typebegrensninger.
Kombinere med Union Typer
Mal-literal-typer skinner virkelig når de brukes med union-typer. Dette lar deg opprette typer som representerer et spesifikt sett av strengkombinasjoner.
type HTTPMethod = "GET" | "POST" | "PUT" | "DELETE";
type APIPath = "users" | "products" | "orders";
type APIEndpoint = `/${APIPath}/${HTTPMethod}`;
// Gyldige API-endepunkter
const getUsers: APIEndpoint = "/users/GET";
const postProducts: APIEndpoint = "/products/POST";
// Ugyldige API-endepunkter (vil resultere i TypeScript-feil)
// const invalidEndpoint: APIEndpoint = "/users/PATCH"; // Error: "/users/PATCH" is not assignable to type "/users/GET" | "/users/POST" | "/users/PUT" | "/users/DELETE" | "/products/GET" | "/products/POST" | ... 3 more ... | "/orders/DELETE".
Nå er APIEndpoint en mer restriktiv type som kun tillater spesifikke kombinasjoner av API-stier og HTTP-metoder. TypeScript vil flagge alle forsøk på å bruke ugyldige kombinasjoner, noe som forbedrer typesikkerheten.
Strengmanipulering med Mal-Literal-Typer
TypeScript tilbyr innebygde strengmanipuleringstyper som fungerer sømløst med mal-literal-typer. Disse typene lar deg transformere strenger ved kompileringstid.
- Uppercase: Konverterer en streng til store bokstaver.
- Lowercase: Konverterer en streng til små bokstaver.
- Capitalize: Gjør den første bokstaven i en streng til stor bokstav.
- Uncapitalize: Gjør den første bokstaven i en streng til liten bokstav.
type Greeting = "hello world";
type UppercaseGreeting = Uppercase; // "HELLO WORLD"
type LowercaseGreeting = Lowercase; // "hello world"
type CapitalizedGreeting = Capitalize; // "Hello world"
type UncapitalizedGreeting = Uncapitalize; // "hello world"
Disse strengmanipuleringstypene er spesielt nyttige for automatisk å generere typer basert på navnekonvensjoner. For eksempel kan du utlede handlingstyper fra hendelsesnavn eller omvendt.
Praktiske Anvendelser av Mal-Literal-Typer
- API-endepunktdefinisjon: Som demonstrert ovenfor, definere API-endepunkter med presise typebegrensninger.
- Hendelseshåndtering: Opprette typer for hendelsesnavn med spesifikke prefikser og suffikser.
- CSS Klassegjenerering: Generere CSS-klassenavn basert på komponentnavn og tilstander.
- Database Spørringsbygging: Sikre typesikkerhet ved konstruksjon av databasespørringer.
Internasjonalt Eksempel: Valutaformatering
Forestil deg å bygge en finansiell applikasjon som støtter flere valutaer. Du kan bruke mal-literal-typer for å håndheve korrekt valutaformatering.
type CurrencyCode = "USD" | "EUR" | "GBP" | "JPY";
type CurrencyFormat = `${number} ${T}`;
const priceUSD: CurrencyFormat<"USD"> = "100 USD"; // Gyldig
const priceEUR: CurrencyFormat<"EUR"> = "50 EUR"; // Gyldig
// const priceInvalid: CurrencyFormat<"USD"> = "100 EUR"; // Error: Type 'string' is not assignable to type `${number} USD`'.
function formatCurrency(amount: number, currency: T): CurrencyFormat {
return `${amount} ${currency}`;
}
const formattedUSD = formatCurrency(250, "USD"); // Type: "250 USD"
const formattedEUR = formatCurrency(100, "EUR"); // Type: "100 EUR"
Dette eksempelet sikrer at valutaverdier alltid formateres med riktig valutakode, noe som forhindrer potensielle feil.
Dypdykk i Betingede Typer
Betingede typer introduserer forgreninglogikk i TypeScript sitt typesystem, slik at du kan definere typer som avhenger av andre typer. Denne funksjonen er utrolig kraftig for å lage svært fleksible og gjenbrukbare type-definisjoner.
Grunnleggende Syntaks og Bruk
Betingede typer bruker infer-nøkkelordet og den ternære operatoren (condition ? trueType : falseType) for å definere typebetingelser.
type IsString = T extends string ? true : false;
type StringCheck = IsString; // type StringCheck = true
type NumberCheck = IsString; // type NumberCheck = false
I dette eksempelet er IsString en betinget type som sjekker om T kan tilordnes string. Hvis den kan det, løses typen til true; ellers løses den til false.
Nøkkelordet infer
Nøkkelordet infer lar deg trekke ut en type fra en annen type. Dette er spesielt nyttig når du arbeider med komplekse typer som funksjonstyper eller array-typer.
type ReturnType any> = T extends (...args: any) => infer R ? R : any;
function add(a: number, b: number): number {
return a + b;
}
type AddReturnType = ReturnType; // type AddReturnType = number
I dette eksempelet trekker ReturnType ut returtypen til en funksjonstype T. infer R-delen av den betingede typen utleder returtypen og tilordner den til typevariabelen R. Hvis T ikke er en funksjonstype, løses typen til any.
Distributive Betingede Typer
Betingede typer blir distributive når den sjekkede typen er en «naked type parameter» (en typeparameter som ikke er pakket inn). Dette betyr at den betingede typen blir brukt på hvert medlem av union-typen separat.
type ToArray = T extends any ? T[] : never;
type NumberOrStringArray = ToArray; // type NumberOrStringArray = string[] | number[]
I dette eksempelet konverterer ToArray en type T til en array-type. Fordi T er en «naked type parameter» (ikke pakket inn i en annen type), blir den betingede typen brukt på number og string separat, noe som resulterer i en union av number[] og string[].
Praktiske Anvendelser av Betingede Typer
- Ekstrahere Returtyper: Som demonstrert ovenfor, ekstrahere returtypen til en funksjon.
- Filtrere Typer fra en Union: Opprette en type som kun inneholder spesifikke typer fra en union.
- Definere Overlastede Funksjonstyper: Opprette forskjellige funksjonstyper basert på inputtyper.
- Opprette Type Guards: Definere funksjoner som begrenser typen til en variabel.
Internasjonalt Eksempel: Håndtering av Ulike Datoformater
Forskjellige regioner i verden bruker ulike datoformater. Du kan bruke betingede typer for å håndtere disse variasjonene.
type DateFormat = "YYYY-MM-DD" | "MM/DD/YYYY" | "DD.MM.YYYY";
type ParseDate = T extends "YYYY-MM-DD"
? { year: number; month: number; day: number; format: "YYYY-MM-DD" }
: T extends "MM/DD/YYYY"
? { month: number; day: number; year: number; format: "MM/DD/YYYY" }
: T extends "DD.MM.YYYY"
? { day: number; month: number; year: number; format: "DD.MM.YYYY" }
: never;
function parseDate(dateString: string, format: T): ParseDate {
// (Implementasjon vil håndtere ulike datoformater)
if (format === "YYYY-MM-DD") {
const [year, month, day] = dateString.split("-").map(Number);
return { year, month, day, format } as ParseDate;
} else if (format === "MM/DD/YYYY") {
const [month, day, year] = dateString.split("/").map(Number);
return { month, day, year, format } as ParseDate;
} else if (format === "DD.MM.YYYY") {
const [day, month, year] = dateString.split(".").map(Number);
return { day, month, year, format } as ParseDate;
} else {
throw new Error("Ugyldig datoformat");
}
}
const parsedDateISO = parseDate("2023-10-27", "YYYY-MM-DD"); // Type: { year: number; month: number; day: number; format: "YYYY-MM-DD"; }
const parsedDateUS = parseDate("10/27/2023", "MM/DD/YYYY"); // Type: { month: number; day: number; year: number; format: "MM/DD/YYYY"; }
const parsedDateEU = parseDate("27.10.2023", "DD.MM.YYYY"); // Type: { day: number; month: number; year: number; format: "DD.MM.YYYY"; }
console.log(parsedDateISO.year); // Få tilgang til året, vel vitende om at det vil være der
Dette eksempelet bruker betingede typer for å definere forskjellige dato-parsing-funksjoner basert på det spesifiserte datoformatet. ParseDate-typen sikrer at det returnerte objektet har de korrekte egenskapene basert på formatet.
Kombinere Mal-Literal- og Betingede Typer
Den virkelige kraften kommer når du kombinerer mal-literal-typer og betingede typer. Dette muliggjør utrolig kraftige type-manipulasjoner.
type EventName = `on${Capitalize}`;
type ExtractEventPayload = T extends EventName
? { type: T; payload: any } // Forenklet for demonstrasjon
: never;
type ClickEvent = EventName<"click">; // "onClick"
type MouseOverEvent = EventName<"mouseOver">; // "onMouseOver"
// Eksempel på funksjon som tar en type
function processEvent(event: T): ExtractEventPayload {
// I en ekte implementering ville vi faktisk sendt hendelsen.
console.log(`Processing event ${event}`);
// I en ekte implementering ville "payload" vært basert på hendelsestype.
return { type: event, payload: {} } as ExtractEventPayload;
}
// Merk at returtypene er svært spesifikke:
const clickEvent = processEvent("onClick"); // { type: "onClick"; payload: any; }
const mouseOverEvent = processEvent("onMouseOver"); // { type: "onMouseOver"; payload: any; }
// Hvis du bruker andre strenger, får du "never":
// const someOtherEvent = processEvent("someOtherEvent"); // Type er `never`
Beste Praksis og Betraktninger
- Hold det enkelt: Selv om de er kraftige, kan disse avanserte typene raskt bli komplekse. Streb etter klarhet og vedlikeholdbarhet.
- Test grundig: Sørg for at type-definisjonene dine fungerer som forventet ved å skrive omfattende enhetstester.
- Dokumenter koden din: Dokumenter tydelig formålet og oppførselen til dine avanserte typer for å forbedre kodelesbarheten.
- Vurder ytelse: Overdreven bruk av avanserte typer kan påvirke kompileringstiden. Profiler koden din og optimaliser der det er nødvendig.
Konklusjon
Mal-literal-typer og betingede typer er kraftige verktøy i TypeScript sitt arsenal. Ved å mestre disse avanserte typene kan du skrive mer uttrykksfull, vedlikeholdbar og typesikker kode. Disse funksjonene gjør det mulig å fange komplekse relasjoner mellom typer, håndheve strengere begrensninger og skape svært gjenbrukbare type-definisjoner. Omfavn disse teknikkene for å heve dine TypeScript-ferdigheter og bygge robuste og skalerbare applikasjoner for et globalt publikum.