Sužinokite, kaip pasiekti tipo saugų, kompiliavimo laiku patikrintą šablonų atitikimą JavaScript naudojant TypeScript, diskriminuotas sąjungas ir šiuolaikines bibliotekas.
JavaScript Šablonų Atitikimas ir Tipo Sauga: Kompiliavimo Laiko Patikrinimo Vadovas
Šablonų atitikimas yra viena iš galingiausių ir išraiškingiausių savybių šiuolaikiniame programavime, ilgai švenčiama funkcinėse kalbose, tokiose kaip Haskell, Rust ir F#. Tai leidžia kūrėjams išskaidyti duomenis ir vykdyti kodą pagal jo struktūrą taip, kad būtų ir glausta, ir neįtikėtinai lengvai skaitoma. JavaScript toliau tobulėjant, kūrėjai vis dažniau siekia pritaikyti šias galingas paradigmas. Tačiau išlieka didelis iššūkis: kaip užtikrinti tvirtą šių kalbų tipo saugą ir kompiliavimo laiko garantijas dinamiškame JavaScript pasaulyje?
Atsakymas slypi naudojant statinę TypeScript tipo sistemą. Nors pats JavaScript pamažu artėja prie natūralaus šablonų atitikimo, jo dinamiškumas reiškia, kad bet kokie patikrinimai vyktų vykdymo metu, o tai gali sukelti netikėtų klaidų gamyboje. Šis straipsnis yra gilus nardymas į metodus ir įrankius, kurie leidžia atlikti tikrą kompiliavimo laiko šablonų patikrinimą, užtikrinant, kad klaidas pastebėsite ne tada, kai tai padaro jūsų vartotojai, o tada, kai rašote kodą.
Mes išnagrinėsime, kaip sukurti patikimas, savaime dokumentuotas ir klaidoms atsparias sistemas derinant galingas TypeScript funkcijas su šablonų atitikimo elegancija. Pasiruoškite pašalinti visą vykdymo laiko klaidų klasę ir rašyti kodą, kurį saugiau ir lengviau prižiūrėti.
Kas Iš Tikrųjų Yra Šablonų Atitikimas?
Iš esmės šablonų atitikimas yra sudėtingas valdymo srauto mechanizmas. Tai tarsi itin galingas `switch` sakinys. Vietoj to, kad tiesiog tikrintumėte, ar lygybė atitinka paprastas reikšmes (pvz., skaičius ar eilutes), šablonų atitikimas leidžia patikrinti reikšmę pagal sudėtingus „šablonus“ ir, jei atitiktis randama, susieti kintamuosius su tos reikšmės dalimis.
Palyginkime tai su tradiciniais metodais:
Senasis Būdas: `if-else` Grandinės ir `switch`
Įsivaizduokite funkciją, kuri apskaičiuoja geometrinės figūros plotą. Naudojant tradicinį metodą, jūsų kodas gali atrodyti taip:
// Shape is an object with a 'type' property
function calculateArea(shape) {
if (shape.type === 'circle') {
return Math.PI * shape.radius * shape.radius;
} else if (shape.type === 'square') {
return shape.sideLength * shape.sideLength;
} else if (shape.type === 'rectangle') {
return shape.width * shape.height;
} else {
throw new Error('Unsupported shape type');
}
}
Tai veikia, bet yra daugiažodžiavimas ir linkęs į klaidas. Ką daryti, jei pridėsite naują formą, pvz., `triangle`, bet pamiršite atnaujinti šią funkciją? Kodas vykdymo metu išmes bendrą klaidą, kuri gali būti toli nuo vietos, kur buvo įvesta tikroji klaida.
Šablonų Atitikimo Būdas: Deklaratyvus ir Išraiškingas
Šablonų atitikimas perkelia šią logiką, kad ji būtų deklaratyvesnė. Vietoj imperatyvių patikrinimų serijos, jūs deklaruojate šablonus, kurių tikitės, ir veiksmus, kurių reikia imtis:
// Pseudocode for a future JavaScript pattern matching feature
function calculateArea(shape) {
match (shape) {
when ({ type: 'circle', radius }): return Math.PI * radius * radius;
when ({ type: 'square', sideLength }): return sideLength * sideLength;
when ({ type: 'rectangle', width, height }): return width * height;
default: throw new Error('Unsupported shape type');
}
}
Pagrindiniai pranašumai iš karto akivaizdūs:
- Dezintegravimas: Reikšmės, tokios kaip `radius`, `width` ir `height`, automatiškai ištraukiamos iš `shape` objekto.
- Skaitymo galimybė: Kodo paskirtis yra aiškesnė. Kiekviena `when` sąlyga apibūdina konkrečią duomenų struktūrą ir atitinkamą logiką.
- Išsamumas: Tai yra pats svarbiausias pranašumas tipo saugai. Iš tiesų patikima šablonų atitikimo sistema gali jus įspėti kompiliavimo metu, jei pamiršote apdoroti galimą atvejį. Tai yra mūsų pagrindinis tikslas.
JavaScript Iššūkis: Dinamiškumas prieš Saugą
Didžiausia JavaScript stiprybė – jo lankstumas ir dinamiškumas – taip pat yra didžiausias jo trūkumas, kai kalbama apie tipo saugą. Be statinės tipo sistemos, įgyvendinančios sutartis kompiliavimo metu, šablonų atitikimas paprastame JavaScript apsiriboja vykdymo laiko patikrinimais. Tai reiškia:
- Jokių Kompiliavimo Laiko Garantijų: Jūs nežinosite, kad praleidote atvejį, kol jūsų kodas nebus paleistas ir nepasieks to konkretaus kelio.
- Tylūs Gedimai: Jei pamiršite numatytąjį atvejį, neatitinkanti reikšmė gali tiesiog sukelti `undefined`, sukeldama subtilias klaidas toliau.
- Refaktoriavimo Košmarai: Pridėjus naują variantą į duomenų struktūrą (pvz., naują įvykio tipą, naują API atsakymo būseną), reikia atlikti visuotinę paiešką ir pakeitimą, kad būtų rastos visos vietos, kur ją reikia apdoroti. Praleidus vieną, gali sugesti jūsų programa.
Štai čia TypeScript visiškai pakeičia žaidimą. Jo statinė tipo sistema leidžia mums tiksliai modeliuoti savo duomenis ir tada panaudoti kompiliatorių, kad užtikrintume, jog apdorojame kiekvieną galimą variantą. Išnagrinėkime, kaip.
Technika 1: Pagrindas su Diskriminuotomis Sąjungomis
Vienintelė svarbiausia TypeScript funkcija, leidžianti saugiai atitikti šablonus pagal tipą, yra diskriminuota sąjunga (taip pat žinoma kaip pažymėta sąjunga arba algebrinis duomenų tipas). Tai galingas būdas modeliuoti tipą, kuris gali būti vienas iš kelių skirtingų galimybių.
Kas Yra Diskriminuota Sąjunga?
Diskriminuota sąjunga sudaryta iš trijų komponentų:
- Aibė skirtingų tipų (sąjungos nariai).
- Bendra savybė su literaliniu tipu, žinoma kaip diskriminantas arba žyma. Ši savybė leidžia TypeScript susiaurinti konkretų tipą sąjungoje.
- Sąjungos tipas, kuris sujungia visus nario tipus.
Pakeiskime mūsų formos pavyzdį naudodami šį šabloną:
// 1. Define the distinct member types
interface Circle {
kind: 'circle'; // The discriminant
radius: number;
}
interface Square {
kind: 'square'; // The discriminant
sideLength: number;
}
interface Rectangle {
kind: 'rectangle'; // The discriminant
width: number;
height: number;
}
// 2. Create the union type
type Shape = Circle | Square | Rectangle;
Dabar `Shape` tipo kintamasis turi būti viena iš šių trijų sąsajų. Savybė `kind` veikia kaip raktas, kuris atrakina TypeScript tipo susiaurinimo galimybes.
Kompiliavimo Laiko Išsamumo Tikrinimo Įgyvendinimas
Kai turime diskriminuotą sąjungą, dabar galime parašyti funkciją, kurią kompiliatorius garantuoja apdoroti kiekvieną įmanomą formą. Magiškas ingredientas yra TypeScript `never` tipas, kuris vaizduoja reikšmę, kurios niekada neturėtų būti.
Galime parašyti paprastą pagalbinę funkciją, kad tai įgyvendintume:
function assertUnreachable(x: never): never {
throw new Error("Didn't expect to get here");
}
Dabar perrašykime savo `calculateArea` funkciją naudodami standartinį `switch` sakinį. Stebėkite, kas atsitinka `default` atveju:
function calculateArea(shape: Shape): number {
switch (shape.kind) {
case 'circle':
// TypeScript knows `shape` is a Circle here!
return Math.PI * shape.radius ** 2;
case 'square':
// TypeScript knows `shape` is a Square here!
return shape.sideLength ** 2;
case 'rectangle':
// TypeScript knows `shape` is a Rectangle here!
return shape.width * shape.height;
default:
// If we've handled all cases, `shape` will be of type `never`
return assertUnreachable(shape);
}
}
Šis kodas kompiliuojamas puikiai. Kiekviename `case` bloke TypeScript susiaurino `shape` tipą iki `Circle`, `Square` arba `Rectangle`, leisdamas mums saugiai pasiekti tokias savybes kaip `radius`.
Dabar magiškas momentas. Pristatykime naują formą į mūsų sistemą:
interface Triangle {
kind: 'triangle';
base: number;
height: number;
}
type Shape = Circle | Square | Rectangle | Triangle; // Add it to the union
Kai tik pridėsime `Triangle` prie `Shape` sąjungos, mūsų `calculateArea` funkcija iškart sukurs kompiliavimo laiko klaidą:
// In the `default` block of `calculateArea`:
return assertUnreachable(shape);
// ~~~~~
// Argument of type 'Triangle' is not assignable to parameter of type 'never'.
Ši klaida yra neįtikėtinai vertinga. TypeScript kompiliatorius mums sako: „Pažadėjote apdoroti kiekvieną įmanomą `Shape`, bet pamiršote apie `Triangle`. Kintamasis `shape` vis dar gali būti `Triangle` numatytuoju atveju, ir tai nėra priskiriama `never`“.
Norėdami ištaisyti klaidą, tiesiog pridėkite trūkstamą atvejį. Kompiliatorius tampa mūsų saugos tinklu, garantuojančiu, kad mūsų logika išliks sinchronizuota su mūsų duomenų modeliu.
// ... inside the switch
case 'triangle':
return 0.5 * shape.base * shape.height;
default:
return assertUnreachable(shape);
// ... now the code compiles again!
Šio Metodo Privalumai ir Trūkumai
- Privalumai:
- Nulinės Priklausomybės: Jis naudoja tik pagrindines TypeScript funkcijas.
- Maksimalus Tipo Saugumas: Suteikia tvirtas kompiliavimo laiko garantijas.
- Puikus Našumas: Jis kompiliuojamas į labai optimizuotą standartinį JavaScript `switch` sakinį.
- Trūkumai:
- Daugiažodiškumas: `switch`, `case`, `break`/`return` ir `default` standartinė formulė gali atrodyti varginanti.
- Ne Išraiška: `switch` sakinio negalima tiesiogiai grąžinti arba priskirti kintamajam, todėl atsiranda daugiau imperatyvių kodo stilių.
Technika 2: Ergonomiškos API su Šiuolaikinėmis Bibliotekomis
Nors diskriminuota sąjunga su `switch` sakiniu yra pagrindas, jos standartinė formulė gali būti nuobodi. Dėl to išpopuliarėjo fantastiškos atvirojo kodo bibliotekos, kurios suteikia funkcionalesnę, išraiškingesnę ir ergonomiškesnę API šablonų atitikimui, tuo pačiu panaudodamos TypeScript kompiliatorių saugai.
Pristatome `ts-pattern`
Viena iš populiariausių ir galingiausių bibliotekų šioje srityje yra `ts-pattern`. Tai leidžia pakeisti `switch` sakinius sklandžia, grandinine API, kuri veikia kaip išraiška.
Perrašykime savo `calculateArea` funkciją naudodami `ts-pattern`:
import { match } from 'ts-pattern';
function calculateAreaWithTsPattern(shape: Shape): number {
return match(shape)
.with({ kind: 'circle' }, (s) => Math.PI * s.radius ** 2)
.with({ kind: 'square' }, (s) => s.sideLength ** 2)
.with({ kind: 'rectangle' }, (s) => s.width * s.height)
.with({ kind: 'triangle' }, (s) => 0.5 * s.base * s.height)
.exhaustive(); // This is the key to compile-time safety
}
Panagrinėkime, kas vyksta:
- `match(shape)`: Tai pradeda šablonų atitikimo išraišką, paimant reikšmę, kuri bus atitikta.
- `.with({ kind: '...' }, handler)`: Kiekvienas `.with()` iškvietimas apibrėžia šabloną. `ts-pattern` yra pakankamai protingas, kad numatytų antrojo argumento (funkcijos `handler`) tipą. Šablonui `{ kind: 'circle' }` jis žino, kad įvesties `s` į tvarkyklę tipas bus `Circle`.
- `.exhaustive()`: Šis metodas yra lygiavertis mūsų `assertUnreachable` triukui. Jis nurodo `ts-pattern`, kad turi būti apdoroti visi galimi atvejai. Jei pašalintume eilutę `.with({ kind: 'triangle' }, ...)` , `ts-pattern` suaktyvintų kompiliavimo laiko klaidą `.exhaustive()` iškvietime, pranešdama mums, kad atitikimas nėra išsamus.
Išplėstinės `ts-pattern` Funkcijos
`ts-pattern` yra daug daugiau nei paprastas savybių atitikimas:
- Predikato Atitikimas su `.when()`: Atitikimas pagal sąlygą.
match(input) .when(isString, (str) => `It's a string: ${str}`) .when(isNumber, (num) => `It's a number: ${num}`) .otherwise(() => 'It is something else'); - Giliai Įdėti Šablonai: Atitikimas su sudėtingomis objektų struktūromis.
match(user) .with({ address: { city: 'Paris' } }, () => 'User is in Paris') .otherwise(() => 'User is elsewhere'); - Pakaitos Simboliai ir Specialūs Selektoriai: Naudokite `P.select()`, kad užfiksuotumėte reikšmę šablone, arba `P.string`, `P.number`, kad atitiktų bet kokią tam tikro tipo reikšmę.
import { match, P } from 'ts-pattern'; match(event) .with({ type: 'USER_LOGIN', user: { name: P.select() } }, (name) => { console.log(`${name} logged in.`); }) .otherwise(() => {});
Naudodami tokią biblioteką kaip `ts-pattern`, gausite geriausią iš abiejų pasaulių: tvirtą kompiliavimo laiko saugą, kurią užtikrina TypeScript `never` tikrinimas, kartu su švaria, deklaratyvia ir labai išraiškinga API.
Ateitis: TC39 Šablonų Atitikimo Pasiūlymas
Pati JavaScript kalba eina keliu, kad gautų natūralų šablonų atitikimą. Yra aktyvus pasiūlymas TC39 (komitetui, kuris standartizuoja JavaScript), kad į kalbą būtų įtraukta `match` išraiška.
Siūloma Sintaksė
Sintaksė greičiausiai atrodys maždaug taip:
// This is proposed JavaScript syntax and might change
const getMessage = (response) => {
return match (response) {
when ({ status: 200, body: b }) { return `Success with body: ${b}`; }
when ({ status: 404 }) { return 'Not Found'; }
when ({ status: s if s >= 500 }) { return `Server Error: ${s}`; }
default { return 'Unknown response'; }
}
};
O Kaip Su Tipo Sauga?
Tai yra esminis klausimas mūsų diskusijai. Pati savaime, natūrali JavaScript šablonų atitikimo funkcija atliktų savo patikrinimus vykdymo metu. Ji nežinotų apie jūsų TypeScript tipus.
Tačiau beveik neabejotina, kad TypeScript komanda sukurtų statinę analizę ant šios naujos sintaksės. Kaip TypeScript analizuoja `if` sakinius ir `switch` blokus, kad atliktų tipo susiaurinimą, ji analizuotų `match` išraiškas. Tai reiškia, kad galiausiai galėtume gauti geriausią įmanomą rezultatą:
- Natūrali, Naši Sintaksė: Nereikia bibliotekų ar transpiliavimo triukų.
- Visiškas Kompiliavimo Laiko Saugumas: TypeScript patikrintų `match` išraiškos išsamumą pagal diskriminuotą sąjungą, kaip tai daro šiandien su `switch`.
Kol laukiame, kol ši funkcija prasiskins kelią per pasiūlymo etapus ir pateks į naršykles ir vykdymo aplinkas, metodai, kuriuos aptarėme šiandien su diskriminuotomis sąjungomis ir bibliotekomis, yra gamybai paruoštas, pažangiausias sprendimas.
Praktinis Pritaikymas ir Geriausia Praktika
Pažiūrėkime, kaip šie šablonai taikomi įprastiems, realiems kūrimo scenarijams.
Būsenos Valdymas (Redux, Zustand ir kt.)
Būsenos valdymas su veiksmais yra puikus diskriminuotų sąjungų naudojimo atvejis. Vietoj to, kad naudotumėte eilučių konstantas veiksmų tipams, apibrėžkite diskriminuotą sąjungą visiems galimiems veiksmams.
// Define actions
interface IncrementAction { type: 'counter/increment'; payload: number; }
interface DecrementAction { type: 'counter/decrement'; payload: number; }
interface ResetAction { type: 'counter/reset'; }
type CounterAction = IncrementAction | DecrementAction | ResetAction;
// A type-safe reducer
function counterReducer(state: number, action: CounterAction): number {
return match(action)
.with({ type: 'counter/increment' }, (act) => state + act.payload)
.with({ type: 'counter/decrement' }, (act) => state - act.payload)
.with({ type: 'counter/reset' }, () => 0)
.exhaustive();
}
Dabar, jei pridėsite naują veiksmą į `CounterAction` sąjungą, TypeScript privers jus atnaujinti reduktorių. Daugiau pamirštų veiksmų tvarkyklių!
API Atsakymų Apdorojimas
Duomenų gavimas iš API apima kelias būsenas: įkėlimas, sėkmė ir klaida. Modeliavimas su diskriminuota sąjunga daro jūsų vartotojo sąsajos logiką daug patikimesnę.
// Model the async data state
type RemoteData =
| { status: 'idle' }
| { status: 'loading' }
| { status: 'success'; data: T }
| { status: 'error'; error: E };
// In your UI component (e.g., React)
function UserProfile({ userId }: { userId: string }) {
const [userState, setUserState] = useState>({ status: 'idle' });
// ... useEffect to fetch data and update state ...
return match(userState)
.with({ status: 'idle' }, () => Click a button to load the user.
)
.with({ status: 'loading' }, () => )
.with({ status: 'success' }, (state) => )
.with({ status: 'error' }, (state) => )
.exhaustive();
}
Šis metodas garantuoja, kad įdiegėte vartotojo sąsają kiekvienai galimai duomenų gavimo būsenai. Jūs negalite netyčia pamiršti apdoroti įkėlimo ar klaidos atvejo.
Geriausios Praktikos Santrauka
- Modeliuokite su Diskriminuotomis Sąjungomis: Kai tik turite reikšmę, kuri gali būti viena iš kelių skirtingų formų, naudokite diskriminuotą sąjungą. Tai yra tipo saugių šablonų kertinis akmuo TypeScript.
- Visada Užtikrinkite Išsamumą: Nesvarbu, ar naudojate `never` triuką su `switch` sakiniu, ar bibliotekos `.exhaustive()` metodą, niekada nepalikite šablonų atitikimo atviro. Iš čia kyla saugumas.
- Pasirinkite Tinkamą Įrankį: Paprastiems atvejams `switch` sakinys yra tinkamas. Sudėtingai logikai, įdėtam atitikimui ar funkcionalesniam stiliui, tokia biblioteka kaip `ts-pattern` žymiai pagerins skaitomumą ir sumažins standartinę formulę.
- Laikykite Šablonus Lengvai Skaitomais: Tikslas yra aiškumas. Venkite pernelyg sudėtingų, įdėtų šablonų, kuriuos sunku suprasti iš pirmo žvilgsnio. Kartais geriau suskaidyti atitikimą į mažesnes funkcijas.
Išvada: Saugios JavaScript Ateities Rašymas
Šablonų atitikimas yra daugiau nei tik sintaksinis saldainis; tai paradigma, kuri veda prie deklaratyvesnio, lengviau skaitomo ir, svarbiausia, patikimesnio kodo. Kol nekantriai laukiame jo natūralaus atvykimo į JavaScript, mums nereikia laukti, kad pasinaudotume jo privalumais.
Panaudodami TypeScript statinės tipo sistemos galią, ypač su diskriminuotomis sąjungomis, galime sukurti sistemas, kurios gali būti patikrintos kompiliavimo metu. Šis metodas iš esmės perkelia klaidų aptikimą iš vykdymo laiko į kūrimo laiką, sutaupydamas daugybę derinimo valandų ir užkertant kelią gamybos incidentams. Tokios bibliotekos kaip `ts-pattern` remiasi šiuo tvirtu pagrindu, suteikdamos elegantišką ir galingą API, dėl kurios tipo saugaus kodo rašymas tampa džiaugsmu.
Kompiliavimo laiko šablonų patikrinimo priėmimas yra žingsnis link atsparesnių ir prižiūrimų programų rašymo. Jis skatina jus aiškiai pagalvoti apie visas galimas būsenas, kuriose gali būti jūsų duomenys, pašalinant dviprasmybes ir padarant jūsų kodo logiką visiškai aiškią. Pradėkite modeliuoti savo domeną su diskriminuotomis sąjungomis šiandien ir leiskite TypeScript kompiliatoriui būti jūsų nenuilstamu partneriu kuriant programinę įrangą be klaidų.