Looge JavaScriptis ja TypeScriptis robustset, tüübikindlat koodi mustrisobituse tüübikaitsete, eristatavate liitude ja ammendavuse kontrolliga. Vältige käitusvigu.
JavaScript'i mustrisobitus-tüübikaitse: juhend tüübikindla mustrisobituse jaoks
Tänapäeva tarkvaraarenduse maailmas on keerukate andmestruktuuride haldamine igapäevane väljakutse. Olenemata sellest, kas käsitlete API vastuseid, haldate rakenduse olekut või töötlete kasutaja sündmusi, puutute sageli kokku andmetega, mis võivad esineda mitmel erineval kujul. Traditsiooniline lähenemine, kasutades pesastatud if-else-lauseid või tavalisi switch-juhtumeid, on sageli sõnaohtlik, vigaderohke ja soodne pinnas käitusvigade tekkeks. Mis oleks, kui kompilaator saaks olla teie turvavõrk, tagades, et olete käsitlenud iga võimalikku stsenaariumi?
Siin tulebki mängu tüübikindla mustrisobituse jõud. Laenates kontseptsioone funktsionaalsetest programmeerimiskeeltest nagu F#, OCaml ja Rust ning kasutades ära TypeScripti võimsat tüübisüsteemi, saame kirjutada koodi, mis pole mitte ainult väljendusrikkam ja loetavam, vaid ka põhimõtteliselt turvalisem. See artikkel on sügav sukeldumine sellesse, kuidas saate saavutada oma JavaScripti ja TypeScripti projektides robustse, tüübikindla mustrisobituse, kõrvaldades terve klassi vigu juba enne koodi käivitamist.
Mis täpselt on mustrisobitus?
Oma olemuselt on mustrisobitus mehhanism väärtuse kontrollimiseks mustrite seeria vastu. See on nagu ülelaetud switch-lause. Selle asemel, et kontrollida lihtsalt võrdsust lihtsate väärtustega (nagu stringid või numbrid), võimaldab mustrisobitus kontrollida teie andmete struktuuri või kuju.
Kujutage ette, et sorteerite füüsilist posti. Te ei kontrolli ainult, kas ümbrik on mõeldud "John Doe'le". Te võite sorteerida erinevate mustrite alusel:
- Kas see on väike, ristkülikukujuline ja margiga ümbrik? See on ilmselt kiri.
- Kas see on suur, polsterdatud ümbrik? See on tõenäoliselt pakk.
- Kas sellel on läbipaistev plastikaken? See on peaaegu kindlasti arve või ametlik kirjavahetus.
Mustrisobitus koodis teeb sama asja. See võimaldab teil kirjutada loogikat, mis ütleb: "Kui mu andmed näevad välja sellised, tee seda. Kui neil on selline kuju, tee midagi muud." See deklaratiivne stiil muudab teie kavatsuse palju selgemaks kui keeruline imperatiivsete kontrollide võrgustik.
Klassikaline probleem: ebaturvaline `switch`-lause
Alustame levinud stsenaariumiga JavaScriptis. Me ehitame graafikarakendust ja peame arvutama erinevate kujundite pindala. Iga kujund on objekt, millel on `kind` omadus, et meile öelda, mis see on.
// Meie kujundite objektid
const circle = { kind: 'circle', radius: 5 };
const square = { kind: 'square', sideLength: 10 };
const rectangle = { kind: 'rectangle', width: 4, height: 8 };
function getArea(shape) {
switch (shape.kind) {
case 'circle':
// PROBLEEM: Miski ei takista meil siin ligi pääsemast shape.sideLength'ile
// ja saamast vastuseks `undefined`. Tulemuseks oleks NaN.
return Math.PI * shape.radius ** 2;
case 'square':
return shape.sideLength ** 2;
case 'rectangle':
return shape.width * shape.height;
}
}
See puhas JavaScripti kood töötab, kuid see on habras. Sellel on kaks suurt probleemi:
- Tüübikindluse puudumine: `'circle'`-juhu sees ei tea JavaScripti käituskeskkond, et `shape`-objektil on garanteeritult `radius`-omadus, aga mitte `sideLength`. Lihtne trükiviga nagu `shape.raduis` või vale eeldus nagu `shape.width`-le ligipääsemine tooks kaasa `undefined` ja põhjustaks käitusvigu (nagu `NaN` või `TypeError`).
- Ammendavuse kontrolli puudumine: Mis juhtub, kui uus arendaja lisab `Triangle` (kolmnurga) kujundi? Kui ta unustab `getArea`-funktsiooni uuendada, tagastab see kolmnurkade puhul lihtsalt `undefined` ning see viga võib jääda märkamatuks, kuni see põhjustab probleeme rakenduse hoopis teises osas. See on vaikne ebaõnnestumine, kõige ohtlikum vigade liik.
Lahenduse 1. osa: alus TypeScripti eristatavate liitudega
Nende probleemide lahendamiseks vajame esmalt viisi, kuidas kirjeldada tüübisüsteemile meie "andmeid, mis võivad olla üks mitmest asjast". TypeScripti eristatavad liidud (tuntud ka kui märgistatud liidud või algebralised andmetüübid) on selleks ideaalne tööriist.
Eristataval liidul on kolm komponenti:
- Erinevate liideste või tüüpide kogum, mis esindavad iga võimalikku varianti.
- Ühine, literaalne omadus (eristaja), mis on olemas kõigis variantides, näiteks `kind: 'circle'`.
- Liittüüp, mis ühendab kõik võimalikud variandid.
`Shape` eristatava liidu loomine
Modelleerime oma kujundeid seda mustrit kasutades:
// 1. Määratle liidesed iga variandi jaoks
interface Circle {
kind: 'circle'; // Eristaja
radius: number;
}
interface Square {
kind: 'square'; // Eristaja
sideLength: number;
}
interface Rectangle {
kind: 'rectangle'; // Eristaja
width: number;
height: number;
}
// 2. Loo liittüüp
type Shape = Circle | Square | Rectangle;
Selle `Shape`-tüübiga oleme TypeScriptile öelnud, et `Shape`-tüüpi muutuja peab olema kas `Circle`, `Square` või `Rectangle`. See ei saa olla midagi muud. See struktuur on tüübikindla mustrisobituse alustala.
Lahenduse 2. osa: tüübikitsendused ja kompilaatoripõhine ammendavus
Nüüd, kui meil on eristatav liit, saab TypeScripti kontrollvoo analüüs oma maagiat teha. Kui me kasutame `switch`-lauset eristaja omaduse (`kind`) peal, on TypeScript piisavalt tark, et kitsendada tüüpi igas `case`-plokis. See toimib võimsa, automaatse tüübikaitsena.
function getArea(shape: Shape): number {
switch (shape.kind) {
case 'circle':
// TypeScript teab, et `shape` on siin `Circle`!
// shape.sideLength'ile ligipääsemine oleks kompileerimisaegne viga.
return Math.PI * shape.radius ** 2;
case 'square':
// TypeScript teab, et `shape` on siin `Square`!
return shape.sideLength ** 2;
case 'rectangle':
// TypeScript teab, et `shape` on siin `Rectangle`!
return shape.width * shape.height;
}
}
Märkate kohest paranemist: `case 'circle'` sees on `shape` tüüp kitsendatud `Shape`-ist `Circle`-iks. Kui proovite ligi pääseda `shape.sideLength`-ile, märgistavad teie koodiredaktor ja TypeScripti kompilaator selle kohe veana. Olete kõrvaldanud terve kategooria käitusvigu, mis on põhjustatud valedele omadustele ligipääsemisest!
Tõelise ohutuse saavutamine ammendavuse kontrolliga
Oleme lahendanud tüübikindluse probleemi, aga mis saab vaiksest ebaõnnestumisest uue kujundi lisamisel? Siin jõustame ammendavuse kontrolli. Me ütleme kompilaatorile: "Sa pead tagama, et ma olen käsitlenud iga viimase kui võimaliku `Shape`-tüübi variandi."
Seda saame saavutada nutika trikiga, kasutades `never`-tüüpi. `never`-tüüp esindab väärtust, mida ei tohiks kunagi esineda. Lisame oma `switch`-lausele `default`-juhu, mis üritab omistada `shape`-i `never`-tüüpi muutujale.
Loome selleks väikese abifunktsiooni:
function assertNever(value: never): never {
throw new Error(`Käsitlemata eristatava liidu liige: ${JSON.stringify(value)}`);
}
Nüüd uuendame oma `getArea`-funktsiooni:
function getArea(shape: Shape): number {
switch (shape.kind) {
case 'circle':
return Math.PI * shape.radius ** 2;
case 'square':
return shape.sideLength ** 2;
case 'rectangle':
return shape.width * shape.height;
default:
// Kui oleme käsitlenud kõik juhud, on `shape` siin `never`-tüüpi.
// Kui mitte, on see käsitlemata tüüp, põhjustades kompileerimisvea.
return assertNever(shape);
}
}
Praegusel hetkel kompileerub kood suurepäraselt. Aga nüüd vaatame, mis juhtub, kui lisame uue `Triangle`-kujundi:
interface Triangle {
kind: 'triangle';
base: number;
height: number;
}
// Lisa uus kujund liitu
type Shape = Circle | Square | Rectangle | Triangle;
Koheselt näitab meie `getArea`-funktsioon `default`-juhul kompileerimisviga:
Argument of type 'Triangle' is not assignable to parameter of type 'never'.
See on revolutsiooniline! Kompilaator toimib nüüd meie turvavõrguna. See sunnib meid uuendama `getArea`-funktsiooni, et käsitleda `Triangle`-juhtu. Vaiksest käitusveast on saanud vali ja selge kompileerimisaegne viga. Viga parandades tagame, et meie loogika on täielik.
function getArea(shape: Shape): number { // Nüüd parandusega
switch (shape.kind) {
// ... teised juhud
case 'rectangle':
return shape.width * shape.height;
case 'triangle': // Lisa uus juhtum
return 0.5 * shape.base * shape.height;
default:
return assertNever(shape);
}
}
Kui lisame `case 'triangle'`, muutub `default`-juhtum mis tahes kehtiva `Shape`-i jaoks kättesaamatuks, `shape`-i tüübiks saab sel hetkel `never`, viga kaob ja meie kood on taas täielik ja korrektne.
`switch`-ist edasi: deklaratiivne mustrisobitus teekidega
Kuigi ammendavuse kontrolliga `switch`-lause on uskumatult võimas, võib selle süntaks siiski tunduda veidi sõnaohtlik. Funktsionaalse programmeerimise maailm on juba ammu eelistanud väljenduspõhisemat, deklaratiivsemat lähenemist mustrisobitusele. Õnneks pakub JavaScripti ökosüsteem suurepäraseid teeke, mis toovad selle elegantse süntaksi TypeScripti, koos täieliku tüübikindluse ja ammendavusega.
Üks populaarsemaid ja võimsamaid teeke selleks on `ts-pattern`.
Refaktoorimine `ts-pattern`-iga
Vaatame, kuidas meie `getArea`-funktsioon `ts-pattern`-iga ümber kirjutatuna välja näeb:
import { match, P } from 'ts-pattern';
function getAreaWithTsPattern(shape: Shape): number {
return match(shape)
.with({ kind: 'circle' }, (c) => Math.PI * c.radius ** 2)
.with({ kind: 'square' }, (s) => s.sideLength ** 2)
.with({ kind: 'rectangle' }, (r) => r.width * r.height)
.with({ kind: 'triangle' }, (t) => 0.5 * t.base * t.height)
.exhaustive(); // Tagab, et kõik juhud on käsitletud, täpselt nagu meie `never`-kontroll!
}
See lähenemine pakub mitmeid eeliseid:
- Deklaratiivne ja väljendusrikas: Kood loeb nagu reeglite seeria, öeldes selgelt "kui sisend vastab sellele mustrile, käivita see funktsioon."
- Tüübikindlad tagasikutsed: Pange tähele, et `.with({ kind: 'circle' }, (c) => ...)` puhul tuletatakse `c` tüübiks automaatselt ja korrektselt `Circle`. Saate tagasikutse funktsiooni sees täieliku tüübikindluse ja automaatse täiendamise.
- Sisseehitatud ammendavus: `.exhaustive()`-meetod täidab sama eesmärki kui meie `assertNever`-abifunktsioon. Kui lisate `Shape`-liitu uue variandi, kuid unustate lisada sellele `.with()`-klausli, tekitab `ts-pattern` kompileerimisvea.
- See on väljend: Kogu `match`-plokk on väljend, mis tagastab väärtuse, võimaldades teil seda kasutada otse `return`-lausetes või muutuja omistamistes, mis võib koodi puhtamaks muuta.
`ts-pattern`-i täiustatud võimalused
`ts-pattern` läheb palju kaugemale lihtsast eristajaga sobitamisest. See võimaldab uskumatult võimsaid ja keerukaid mustreid.
- Predikaadiga sobitamine `.when()`-iga: Saate sobitada tingimuse alusel.
- Metamärgiga sobitamine `P.any` ja `P.string` jne: Sobita objekti kuju alusel ilma eristajata.
- Vaikimisi juhtum `.otherwise()`-iga: Pakub puhta viisi käsitleda kõiki juhtumeid, mida pole selgesõnaliselt sobitatud, alternatiivina `.exhaustive()`-ile.
// Käsitle suuri ruute erinevalt
.with({ kind: 'square' }, (s) => s.sideLength ** 2)
// Saab olema:
.with({ kind: 'square' }, s => s.sideLength > 100, (s) => /* eriloogika suurte ruutude jaoks */)
.with({ kind: 'square' }, (s) => s.sideLength ** 2)
// Sobita mis tahes objektiga, millel on numbriline `radius` omadus
.with({ radius: P.number }, (obj) => `Leiti ringisarnane objekt raadiusega ${obj.radius}`)
.with({ kind: 'circle' }, (c) => /* ... */)
.otherwise((shape) => `Toetamata kujund: ${shape.kind}`)
Praktilised kasutusjuhud globaalsele publikule
See muster ei ole mõeldud ainult geomeetriliste kujundite jaoks. See on uskumatult kasulik paljudes reaalsetes programmeerimisstsenaariumides, millega arendajad üle maailma igapäevaselt kokku puutuvad.
1. API päringu olekute haldamine
Levinud ülesanne on andmete pärimine API-st. Selle päringu olek võib tavaliselt olla üks mitmest võimalusest: algne, laadimine, õnnestunud või viga. Eristatav liit on selle modelleerimiseks ideaalne.
interface StateInitial {
status: 'initial';
}
interface StateLoading {
status: 'loading';
}
interface StateSuccess {
status: 'success';
data: T;
}
interface StateError {
status: 'error';
error: Error;
}
type RequestState = StateInitial | StateLoading | StateSuccess | StateError;
// Teie UI komponendis (nt React, Vue, Svelte, Angular)
function renderComponent(state: RequestState) {
return match(state)
.with({ status: 'initial' }, () => Tere tulemast! Klõpsake nuppu oma profiili laadimiseks.
)
.with({ status: 'loading' }, () => )
.with({ status: 'success' }, (s) => )
.with({ status: 'error' }, (e) => )
.exhaustive();
}
Selle mustriga on võimatu kogemata renderdada kasutajaprofiili, kui olek on veel laadimisel, või proovida ligi pääseda `state.data`-le, kui staatus on `error`. Kompilaator tagab teie kasutajaliidese loogilise järjepidevuse.
2. Olekuhaldus (nt Redux, Zustand)
Olekuhalduses saadate tegevusi (actions) rakenduse oleku uuendamiseks. Need tegevused on klassikaline kasutusjuht eristatavate liitude jaoks.
type CartAction =
| { type: 'ADD_ITEM'; payload: { itemId: string; quantity: number } }
| { type: 'REMOVE_ITEM'; payload: { itemId: string } }
| { type: 'SET_SHIPPING_METHOD'; payload: { method: 'standard' | 'express' } }
| { type: 'APPLY_DISCOUNT_CODE'; payload: { code: string } };
function cartReducer(state: CartState, action: CartAction): CartState {
switch (action.type) {
case 'ADD_ITEM':
// `action.payload` on siin korrektselt tüübistatud!
// ... loogika eseme lisamiseks
return { ...state, /* uuendatud esemed */ };
case 'REMOVE_ITEM':
// ... loogika eseme eemaldamiseks
return { ...state, /* uuendatud esemed */ };
// ... ja nii edasi
default:
return assertNever(action);
}
}
Kui `CartAction`-liitu lisatakse uus tegevuse tüüp, ei kompileeru `cartReducer` enne, kui uus tegevus on käsitletud, vältides selle loogika rakendamise unustamist.
3. Sündmuste töötlemine
Olenemata sellest, kas käsitlete WebSocketi sündmusi serverist või kasutaja interaktsiooni sündmusi keerulises rakenduses, pakub mustrisobitus puhta ja skaleeritava viisi sündmuste suunamiseks õigetesse käsitlejatesse.
type SystemEvent =
| { event: 'userLoggedIn'; userId: string; timestamp: number }
| { event: 'userLoggedOut'; userId: string; timestamp: number }
| { event: 'paymentReceived'; amount: number; currency: string; transactionId: string };
function processEvent(event: SystemEvent) {
match(event)
.with({ event: 'userLoggedIn' }, (e) => console.log(`Kasutaja ${e.userId} logis sisse.`))
.with({ event: 'paymentReceived', currency: 'USD' }, (e) => handleUsdPayment(e.amount))
.otherwise((e) => console.log(`Käsitlemata sündmus: ${e.event}`));
}
Eeliste kokkuvõte
- Kuulikindel tüübikindlus: Kõrvaldate terve klassi käitusvigu, mis on seotud valede andmekujudega (nt `Cannot read properties of undefined`).
- Selgus ja loetavus: Mustrisobituse deklaratiivne olemus muudab programmeerija kavatsuse ilmseks, mis viib koodini, mida on lihtsam lugeda ja mõista.
- Garanteeritud täielikkus: Ammendavuse kontroll muudab kompilaatori valvsaks partneriks, mis tagab, et olete käsitlenud iga võimalikku andmevarianti.
- Vaevatu refaktoorimine: Uute variantide lisamine oma andmemudelitesse muutub turvaliseks, juhendatud protsessiks. Kompilaator osutab igale asukohale teie koodibaasis, mis vajab uuendamist.
- Vähendatud standardkood: Teegid nagu `ts-pattern` pakuvad lühikest, võimsat ja elegantset süntaksit, mis on sageli palju puhtam kui traditsioonilised kontrollvoo laused.
Kokkuvõte: Võtke omaks kompileerimisaegne enesekindlus
Liikumine traditsioonilistelt, ebaturvalistelt kontrollvoo struktuuridelt tüübikindlale mustrisobitusele on paradigma muutus. See seisneb kontrollide viimises käitusajast, kus need avalduvad vigadena teie kasutajatele, kompileerimisaega, kus need ilmuvad abistavate vigadena teile, arendajale. Kombineerides TypeScripti eristatavaid liite ammendavuse kontrolli võimsusega – kas manuaalse `never`-kinnituse või teegi nagu `ts-pattern` kaudu – saate ehitada rakendusi, mis on robustsemad, hooldatavamad ja muutustele vastupidavamad.
Järgmine kord, kui leiate end kirjutamast pikka `if-else if-else` ahelat või `switch`-lauset stringi omaduse peal, võtke hetk ja kaaluge, kas saate oma andmeid modelleerida eristatava liiduna. Investeerige tüübikindlusse. Teie tulevane mina ja teie globaalne kasutajaskond tänavad teid stabiilsuse ja usaldusväärsuse eest, mida see teie tarkvarale toob.