Gå utover grunnleggende typinger. Mestre avanserte TypeScript-funksjoner som betingede typer, malteksttyper og strengmanipulering for å bygge robuste og typesikre API-er. En omfattende guide for globale utviklere.
Lås opp TypeScript sitt fulle potensial: En dypdykk i betingede typer, malteksttyper og avansert strengmanipulering
I en verden av moderne programvareutvikling har TypeScript utviklet seg langt utover sin opprinnelige rolle som en enkel type-sjekker for JavaScript. Det har blitt et sofistikert verktøy for det som kan beskrives som type-nivå programmering. Dette paradigmet tillater utviklere å skrive kode som opererer på typer selv, og skaper dynamiske, selv-dokumenterende og bemerkelsesverdig trygge API-er. Kjernen i denne revolusjonen er tre kraftige funksjoner som fungerer sammen: Betingede typer, malteksttyper og en suite av iboende strengmanipuleringstyper.
For utviklere rundt om i verden som ønsker å heve sine TypeScript-ferdigheter, er det ikke lenger en luksus å forstå disse konseptene – det er en nødvendighet for å bygge skalerbare og vedlikeholdbare applikasjoner. Denne guiden tar deg med på et dypdykk, fra de grunnleggende prinsippene og bygger opp til komplekse, virkelige mønstre som demonstrerer deres kombinerte kraft. Enten du bygger et designsystem, en typesikker API-klient eller et komplekst datahåndteringsbibliotek, vil det å mestre disse funksjonene fundamentalt endre måten du skriver TypeScript på.
Grunnlaget: Betingede typer (The `extends` Ternary)
I sin kjerne lar en betinget type deg velge en av to mulige typer basert på en type relasjonssjekk. Hvis du er kjent med JavaScripts ternære operator (condition ? valueIfTrue : valueIfFalse), vil du finne syntaksen umiddelbart intuitiv:
type Result = SomeType extends OtherType ? TrueType : FalseType;
Her fungerer extends-nøkkelordet som vår betingelse. Den sjekker om SomeType kan tilordnes OtherType. La oss bryte det ned med et enkelt eksempel.
Grunnleggende eksempel: Sjekke en type
Tenk deg at vi ønsker å opprette en type som løser til true hvis en gitt type T er en streng, og false ellers.
type IsString
Vi kan deretter bruke denne typen slik:
type A = IsString<"hello">; // type A er true
type B = IsString<123>; // type B er false
Dette er den grunnleggende byggesteinen. Men den sanne kraften til betingede typer frigjøres når den kombineres med infer-nøkkelordet.
Kraften i `infer`: Uttrekking av typer fra innsiden
infer-nøkkelordet er en game-changer. Det lar deg deklarere en ny generisk typevariabel innenfor extends-klausulen, og effektivt fange en del av typen du sjekker. Tenk på det som en type-nivå variabeldeklarasjon som får sin verdi fra mønstermatching.
Et klassisk eksempel er å pakke ut typen som finnes i et Promise.
type UnwrapPromise
La oss analysere dette:
T extends Promise: Dette sjekker omTer etPromise. Hvis det er det, forsøker TypeScript å matche strukturen.infer U: Hvis matchen er vellykket, fanger TypeScript typen somPromiseløser til og legger den i en ny typevariabel kaltU.? U : T: Hvis betingelsen er sann (Tvar etPromise), er den resulterende typenU(den utpakkede typen). Ellers er den resulterende typen bare den opprinnelige typenT.
Bruk:
type User = { id: number; name: string; };
type UserPromise = Promise
type UnwrappedUser = UnwrapPromise
type UnwrappedNumber = UnwrapPromise
Dette mønsteret er så vanlig at TypeScript inkluderer innebygde verktøytyper som ReturnType, som er implementert ved hjelp av det samme prinsippet for å trekke ut returtypen til en funksjon.
Distributive betingede typer: Arbeide med unioner
En fascinerende og avgjørende oppførsel av betingede typer er at de blir distributive når typen som sjekkes er en «naken» generisk typeparameter. Dette betyr at hvis du sender en unionstype til den, vil betingelsen bli brukt på hvert medlem av unionen individuelt, og resultatene vil bli samlet tilbake i en ny union.
Tenk på en type som konverterer en type til en matrise av den typen:
type ToArray
Hvis vi sender en unionstype til ToArray:
type StrOrNumArray = ToArray
Resultatet er ikke (string | number)[]. Fordi T er en naken typeparameter, distribueres betingelsen:
ToArrayblirstring[]ToArrayblirnumber[]
Det endelige resultatet er unionen av disse individuelle resultatene: string[] | number[].
Denne distributive egenskapen er utrolig nyttig for å filtrere unioner. For eksempel bruker den innebygde Extract-verktøytypen dette for å velge medlemmer fra union T som kan tilordnes U.
Hvis du trenger å forhindre denne distributive oppførselen, kan du pakke typeparameteren i en tuppel på begge sider av extends-klausulen:
type ToArrayNonDistributive
type StrOrNumArrayUnified = ToArrayNonDistributive
Med dette solide grunnlaget, la oss utforske hvordan vi kan konstruere dynamiske strengtyper.
Bygge dynamiske strenger på typenivå: Malteksttyper
Introdusert i TypeScript 4.1, lar malteksttyper deg definere typer som er formet som JavaScripts maltekststrenger. De lar deg kjede sammen, kombinere og generere nye strengliteraltyper fra eksisterende.
Syntaksen er akkurat det du forventer:
type World = "World";
type Greeting = `Hello, ${World}!`; // type Greeting er "Hello, World!"
Dette kan virke enkelt, men kraften ligger i å kombinere det med unioner og generiske.
Unioner og permutasjoner
Når en malteksttype involverer en union, utvides den til en ny union som inneholder alle mulige strengpermutasjoner. Dette er en kraftig måte å generere et sett med veldefinerte konstanter.
Tenk deg å definere et sett med CSS-marginsegenskaper:
type Side = "top" | "right" | "bottom" | "left";
type MarginProperty = `margin-${Side}`;
Den resulterende typen for MarginProperty er:
"margin-top" | "margin-right" | "margin-bottom" | "margin-left"
Dette er perfekt for å lage typesikre komponentprops eller funksjonsargumenter der bare spesifikke strengformater er tillatt.
Kombinere med generiske
Maltekster skinner virkelig når de brukes med generiske. Du kan opprette fabrikktyper som genererer nye strengliteraltyper basert på noen inndata.
type MakeEventListener
type UserListener = MakeEventListener<"user">; // "onUserChange"
type ProductListener = MakeEventListener<"product">; // "onProductChange"
Dette mønsteret er nøkkelen til å lage dynamiske, typesikre API-er. Men hva om vi trenger å endre tilfellet til strengen, som å endre "user" til "User" for å få "onUserChange"? Det er her strengmanipuleringstyper kommer inn.
Verktøykassen: Iboende strengmanipuleringstyper
For å gjøre maltekster enda kraftigere, gir TypeScript et sett med innebygde typer for å manipulere strengliteraler. Disse er som verktøyfunksjoner, men for typesystemet.
Tilfellemodifikatorer: `Uppercase`, `Lowercase`, `Capitalize`, `Uncapitalize`
Disse fire typene gjør akkurat det navnene deres antyder:
Uppercase: Konverterer hele strengtypen til store bokstaver.type LOUD = Uppercase<"hello">; // "HELLO"Lowercase: Konverterer hele strengtypen til små bokstaver.type quiet = Lowercase<"WORLD">; // "world"Capitalize: Konverterer den første bokstaven i strengtypen til store bokstaver.type Proper = Capitalize<"john">; // "John"Uncapitalize: Konverterer den første bokstaven i strengtypen til små bokstaver.type variable = Uncapitalize<"PersonName">; // "personName"
La oss gå tilbake til vårt forrige eksempel og forbedre det ved hjelp av Capitalize for å generere konvensjonelle hendelsesbehandlernavn:
type MakeEventListener
type UserListener = MakeEventListener<"user">; // "onUserChange"
type ProductListener = MakeEventListener<"product">; // "onProductChange"
Nå har vi alle brikkene. La oss se hvordan de kombineres for å løse komplekse, virkelige problemer.
Syntesen: Kombinere alle tre for avanserte mønstre
Det er her teori møter praksis. Ved å veve sammen betingede typer, maltekster og strengmanipulering, kan vi bygge utrolig sofistikerte og trygge typedefinisjoner.
Mønster 1: Den fullstendig typesikre hendelsesutsenderen
Mål: Opprett en generisk EventEmitter-klasse med metoder som on(), off() og emit() som er fullstendig typesikre. Dette betyr:
- Hendelsesnavnet som sendes til metodene må være en gyldig hendelse.
- Payloaden som sendes til
emit()må samsvare med typen som er definert for den hendelsen. - Callback-funksjonen som sendes til
on()må godta riktig payload-type for den hendelsen.
Først definerer vi et kart over hendelsesnavn til deres payload-typer:
interface EventMap {
"user:created": { userId: number; name: string; };
"user:deleted": { userId: number; };
"product:added": { productId: string; price: number; };
}
Nå kan vi bygge den generiske EventEmitter-klassen. Vi vil bruke en generisk parameter Events som må utvide vår EventMap-struktur.
class TypedEventEmitter
private listeners: { [K in keyof Events]?: ((payload: Events[K]) => void)[] } = {};
// `on`-metoden bruker en generisk `K` som er en nøkkel i vårt Events-kart
on
if (!this.listeners[event]) {
this.listeners[event] = [];
}
this.listeners[event]?.push(callback);
}
// `emit`-metoden sikrer at payloaden samsvarer med hendelsens type
emit
this.listeners[event]?.forEach(callback => callback(payload));
}
}
La oss instansiere og bruke den:
const appEvents = new TypedEventEmitter
// Dette er typesikkert. Payloaden blir riktig utledet som { userId: number; name: string; }
appEvents.on("user:created", (payload) => {
console.log(`User created: ${payload.name} (ID: ${payload.userId})`);
});
// TypeScript vil feile her fordi "user:updated" ikke er en nøkkel i EventMap
// appEvents.on("user:updated", () => {}); // Feil!
// TypeScript vil feile her fordi payloaden mangler 'name'-egenskapen
// appEvents.emit("user:created", { userId: 123 }); // Feil!
Dette mønsteret gir kompileringstidssikkerhet for det som tradisjonelt er en veldig dynamisk og feilutsatt del av mange applikasjoner.
Mønster 2: Typesikker stitilgang for nestede objekter
Mål: Opprett en verktøytype, PathValue, som kan bestemme typen til en verdi i et nestet objekt T ved hjelp av en punktnotasjonsstrengsti P (f.eks. "user.address.city").
Dette er et svært avansert mønster som viser rekursive betingede typer.
Her er implementeringen, som vi vil bryte ned:
type PathValue
? Key extends keyof T
? PathValue
: never
: P extends keyof T
? T[P]
: never;
La oss spore logikken med et eksempel: PathValue
- Første kall:
Per"a.b.c". Dette samsvarer med malteksten`${infer Key}.${infer Rest}`. Keyer utledet som"a".Rester utledet som"b.c".- Første rekursjon: Typen sjekker om
"a"er en nøkkel tilMyObject. Hvis ja, kaller den rekursivtPathValue. - Andre rekursjon: Nå er
P"b.c". Den samsvarer med malteksten igjen. Keyer utledet som"b".Rester utledet som"c".- Typen sjekker om
"b"er en nøkkel tilMyObject["a"]og kaller rekursivtPathValue. - Grunntilfelle: Til slutt er
P"c". Dette samsvarer ikke med`${infer Key}.${infer Rest}`. Type-logikken faller gjennom til den andre betingelsen:P extends keyof T ? T[P] : never. - Typen sjekker om
"c"er en nøkkel tilMyObject["a"]["b"]. Hvis ja, er resultatetMyObject["a"]["b"]["c"]. Hvis ikke, er detnever.
Bruk med en hjelpefunksjon:
declare function get
const myObject = {
user: {
name: "Alice",
address: {
city: "Wonderland",
zip: 12345
}
}
};
const city = get(myObject, "user.address.city"); // const city: string
const zip = get(myObject, "user.address.zip"); // const zip: number
const invalid = get(myObject, "user.email"); // const invalid: never
Denne kraftige typen forhindrer kjøretidsfeil fra skrivefeil i stier og gir perfekt typeinferens for dypt nestede datastrukturer, en vanlig utfordring i globale applikasjoner som håndterer komplekse API-responser.
Beste praksis og ytelseshensyn
Som med alle kraftige verktøy, er det viktig å bruke disse funksjonene med omhu.
- Prioriter lesbarhet: Komplekse typer kan raskt bli uleselige. Bryt dem ned i mindre, velnavngitte hjelpetyper. Bruk kommentarer for å forklare logikken, akkurat som du ville gjort med kompleks kjøretidskode.
- Forstå
never-typen:never-typen er ditt primære verktøy for å håndtere feiltilstander og filtrere unioner i betingede typer. Den representerer en tilstand som aldri skal oppstå. - Vær oppmerksom på rekursjonsgrenser: TypeScript har en rekursjonsdybdegrense for typeinstansiering. Hvis typene dine er for dypt nestet eller uendelig rekursive, vil kompilatoren feile. Forsikre deg om at de rekursive typene dine har et tydelig grunntilfelle.
- Overvåk IDE-ytelsen: Ekstremt komplekse typer kan noen ganger påvirke ytelsen til TypeScript-språkserveren, noe som fører til tregere autofullføring og typesjekking i redigeringsprogrammet ditt. Hvis du opplever nedgang, kan du se om en kompleks type kan forenkles eller brytes ned.
- Vit når du skal stoppe: Disse funksjonene er for å løse komplekse problemer med typesikkerhet og utvikleropplevelse. Ikke bruk dem til å overkonstruere enkle typer. Målet er å forbedre klarhet og sikkerhet, ikke å legge til unødvendig kompleksitet.
Konklusjon
Betingede typer, maltekster og strengmanipuleringstyper er ikke bare isolerte funksjoner; de er et tett integrert system for å utføre sofistikert logikk på typenivå. De gir oss mulighet til å bevege oss utover enkle annotasjoner og bygge systemer som er dypt bevisste på sin egen struktur og begrensninger.
Ved å mestre denne trioen kan du:
- Opprett selvdokumenterende API-er: Typene selv blir dokumentasjonen, og veileder utviklere til å bruke dem riktig.
- Eliminer hele klasser av feil: Typefeil fanges opp ved kompileringstid, ikke av brukere i produksjon.
- Forbedre utvikleropplevelsen: Nyt rik autofullføring og innebygde feilmeldinger selv for de mest dynamiske delene av kodebasen din.
Å omfavne disse avanserte egenskapene transformerer TypeScript fra et sikkerhetsnett til en kraftig partner i utviklingen. Det lar deg kode kompleks forretningslogikk og invarianter direkte inn i typesystemet, og sikrer at applikasjonene dine er mer robuste, vedlikeholdbare og skalerbare for et globalt publikum.