Vabastage TypeScripti jõud meie põhjaliku rekursiivsete tüüpide juhendiga. Õppige modelleerima keerulisi, pesastatud andmestruktuure, nagu puud ja JSON, praktiliste näidetega.
TypeScripti rekursiivsete tüüpide valdamine: sügav sukeldumine iseennast viitavatesse definitsioonidesse
Tarkvaraarenduse maailmas kohtame sageli andmestruktuure, mis on loomult pesastatud või hierarhilised. Mõelge failisüsteemidele, organisatsioonilistele skeemidele, sotsiaalmeedia platvormi kommenteeritud kommentaaridele või JSON-objekti enda struktuurile. Kuidas me neid keerukaid, iseennast viitavaid struktuure tüübikindlalt esindame? Vastus peitub ühes TypeScripti kõige võimsamas funktsioonis: rekursiivsed tüübid.
See põhjalik juhend viib teid teekonnale rekursiivsete tüüpide põhimõistetest kuni täiustatud rakenduste ja parimate tavadeni. Olenemata sellest, kas olete kogenud TypeScripti arendaja, kes soovib oma arusaamist süvendada, või kesktaseme programmeerija, kes soovib lahendada keerukamaid andmemodelleerimise väljakutseid, varustab see artikkel teid teadmistega, et kasutada rekursiivseid tüüpe enesekindlalt ja täpselt.
Mis on rekursiivsed tüübid? Iseviitamise jõud
Oma olemuselt on rekursiivne tüüp tüübi definitsioon, mis viitab iseendale. See on tüübisüsteemi ekvivalent rekursiivsele funktsioonile - funktsioonile, mis kutsub iseennast. See iseennast viitav võime võimaldab meil määratleda tüüpe andmestruktuuride jaoks, millel on suvaline või teadmata sügavus.
Lihtne reaalse maailma analoogia on Vene matrjoška nukkude kontseptsioon. Iga nukk sisaldab väiksemat, identset nukku, mis omakorda sisaldab teist jne. Rekursiivne tüüp saab seda suurepäraselt modelleerida: `Nukk` on tüüp, millel on omadused nagu `värv` ja `suurus`, ning sisaldab ka valikulist omadust, mis on teine `Nukk`.
Ilma rekursiivsete tüüpideta oleksime sunnitud kasutama vähem ohutuid alternatiive nagu `any` või `unknown` või proovima määratleda piiratud arvu pesastustasemeid (nt `Kategooria`, `Alamkategooria`, `AlamAlamkategooria`), mis on habras ja ebaõnnestub niipea, kui on vaja uut pesastustaset. Rekursiivsed tüübid pakuvad elegantset, skaleeritavat ja tüübikindlat lahendust.
Põhilise rekursiivse tüübi määratlemine: lingitud loend
Alustame klassikalise arvutiteaduse andmestruktuuriga: lingitud loendiga. Lingitud loend on sõlmede jada, kus iga sõlm sisaldab väärtust ja viidet (või linki) jada järgmisele sõlmele. Viimane sõlm viitab `null`-ile või `määratlemata`, mis signaliseerib loendi lõppu.
See struktuur on olemuselt rekursiivne. `Sõlm` on määratletud iseenda kaudu. Siin on, kuidas me saame seda TypeScriptis modelleerida:
interface LinkedListNode {
value: number;
next: LinkedListNode | null;
}
Selles näites on `LinkedListNode` liidesel kaks omadust:
- `väärtus`: Sel juhul `number`. Muudame selle hiljem üldiseks.
- `järgmine`: See on rekursiivne osa. Omadus `järgmine` on kas teine `LinkedListNode` või `null`, kui see on loendi lõpp.
Viidates iseendale oma definitsioonis, saab `LinkedListNode` kirjeldada mis tahes pikkusega sõlmede ahelat. Vaatame seda tegevuses:
const node3: LinkedListNode = { value: 3, next: null };
const node2: LinkedListNode = { value: 2, next: node3 };
const node1: LinkedListNode = { value: 1, next: node2 };
// node1 on loendi pea: 1 -> 2 -> 3 -> null
function sumLinkedList(node: LinkedListNode | null): number {
if (node === null) {
return 0;
}
return node.value + sumLinkedList(node.next);
}
console.log(sumLinkedList(node1)); // Väljund: 6
Funktsioon `sumLinkedList` on meie rekursiivse tüübi jaoks ideaalne kaaslane. See on rekursiivne funktsioon, mis töötleb rekursiivset andmestruktuuri. TypeScript mõistab `LinkedListNode` kuju ja pakub täielikku automaatset lõpetamist ja tüübikontrolli, vältides levinud vigu, nagu proovida pääseda juurde `node.next.value`, kui `node.next` võiks olla `null`.
Hierarhiliste andmete modelleerimine: puu struktuur
Kuigi lingitud loendid on lineaarsed, on paljud reaalse maailma andmekogumid hierarhilised. Siin paistavad puu struktuurid silma ja rekursiivsed tüübid on loomulik viis nende modelleerimiseks.
Näide 1: osakonna organisatsiooniline skeem
Mõelge organisatsioonilisele skeemile, kus igal töötajal on juht ja juhid on ka töötajad. Töötaja saab hallata ka teiste töötajate meeskonda.
interface Employee {
id: number;
name: string;
role: string;
reports: Employee[]; // Rekursiivne osa!
}
const ceo: Employee = {
id: 1,
name: 'Alina Sterling',
role: 'CEO',
reports: [
{
id: 2,
name: 'Ben Carter',
role: 'CTO',
reports: [
{
id: 4,
name: 'David Chen',
role: 'Lead Engineer',
reports: []
}
]
},
{
id: 3,
name: 'Carla Rodriguez',
role: 'CFO',
reports: []
}
]
};
Siin sisaldab liides `Employee` omadust `reports`, mis on teiste `Employee` objektide massiiv. See modelleerib elegantselt kogu hierarhiat, olenemata sellest, kui palju juhtimistasemeid on. Saame kirjutada funktsioone selle puu läbimiseks, näiteks konkreetse töötaja leidmiseks või osakonnas olevate inimeste koguarvu arvutamiseks.
Näide 2: failisüsteem
Teine klassikaline puu struktuur on failisüsteem, mis koosneb failidest ja kataloogidest (kaustadest). Kataloog võib sisaldada nii faile kui ka teisi katalooge.
interface File {
type: 'file';
name: string;
size: number; // baitides
}
interface Directory {
type: 'directory';
name: string;
contents: FileSystemNode[]; // Rekursiivne osa!
}
// Diskrimineeritud liit tüübi ohutuse tagamiseks
type FileSystemNode = File | Directory;
const root: Directory = {
type: 'directory',
name: 'project',
contents: [
{
type: 'file',
name: 'package.json',
size: 256
},
{
type: 'directory',
name: 'src',
contents: [
{
type: 'file',
name: 'index.ts',
size: 1024
},
{
type: 'directory',
name: 'components',
contents: []
}
]
}
]
};
Selles täiustatud näites kasutame liittüüpi `FileSystemNode`, et esindada, et üksus võib olla kas `File` või `Directory`. Liides `Directory` kasutab seejärel rekursiivselt oma `contents` jaoks `FileSystemNode`. Omadus `type` toimib diskriminandina, võimaldades TypeScriptil tüüpi õigesti kitsendada `if` või `switch` lausetes.
JSON-iga töötamine: universaalne ja praktiline rakendus
Võib-olla on rekursiivsete tüüpide kõige levinum kasutusjuhtum tänapäevases veebiarenduses JSON-i (JavaScript Object Notation) modelleerimine. JSON-i väärtus võib olla string, number, boolean, null, JSON-i väärtuste massiiv või objekt, mille väärtused on JSON-i väärtused.
Kas märkate rekursiooni? Massiivi elemendid on JSON-i väärtused. Objekti omadused on JSON-i väärtused. See nõuab iseennast viitavat tüübi definitsiooni.
Tüübi määratlemine suvalise JSON-i jaoks
Siin on, kuidas saate määratleda robustse tüübi mis tahes kehtiva JSON-i struktuuri jaoks. See muster on uskumatult kasulik töötamisel API-dega, mis tagastavad dünaamilisi või ettearvamatuid JSON-i koormaid.
type JsonValue =
| string
| number
| boolean
| null
| JsonValue[] // Rekursiivne viide iseenda massiivile
| { [key: string]: JsonValue }; // Rekursiivne viide iseenda objektile
// Selguse huvides on tavaline määratleda ka JsonObject eraldi:
type JsonObject = { [key: string]: JsonValue };
// Ja seejärel määratleda JsonValue ümber järgmiselt:
type JsonValue =
| string
| number
| boolean
| null
| JsonValue[]
| JsonObject;
See on näide vastastikusest rekursioonist. `JsonValue` on määratletud `JsonObject` (või sisseehitatud objekti) kaudu ja `JsonObject` on määratletud `JsonValue` kaudu. TypeScript käsitleb seda tsirkulaarset viidet sujuvalt.
Näide: tüübikindel JSON-i stringify funktsioon
Meie `JsonValue` tüübiga saame luua funktsioone, mis on garanteeritud töötama ainult kehtivate JSON-iga ühilduvate andmestruktuuridega, vältides käitusajal tekkivaid vigu enne nende tekkimist.
function processJson(data: JsonValue): void {
if (typeof data === 'string') {
console.log(`Leidsin stringi: ${data}`);
} else if (Array.isArray(data)) {
console.log('Töötlen massiivi...');
data.forEach(processJson); // Rekursiivne kutse
} else if (typeof data === 'object' && data !== null) {
console.log('Töötlen objekti...');
for (const key in data) {
processJson(data[key]); // Rekursiivne kutse
}
}
// ... käsitle muid primitiivseid tüüpe
}
const myData: JsonValue = {
user: 'Alex',
is_active: true,
session_data: {
id: 12345,
tokens: ['A', 'B', 'C']
}
};
processJson(myData);
Tüübitades parameetri `data` kui `JsonValue`, tagame, et iga katse edastada funktsioon, `Date` objekt, `määratlemata` või mis tahes muu mitte-serialiseeritav väärtus funktsioonile `processJson` põhjustab kompileerimisaja vea. See on tohutu edasiminek koodi vastupidavuses.
Täiustatud kontseptsioonid ja võimalikud ohud
Rekursiivsetesse tüüpidesse süvenedes kohtate täiustatumaid mustreid ja mõningaid levinud väljakutseid.
Üldised rekursiivsed tüübid
Meie algne `LinkedListNode` oli kõvasti kodeeritud kasutama oma väärtuse jaoks `number`-t. See pole eriti korduvkasutatav. Saame muuta selle üldiseks, et toetada mis tahes andmetüüpi.
interface GenericNode<T> {
value: T;
next: GenericNode<T> | null;
}
let stringNode: GenericNode<string> = { value: 'hello', next: null };
let numberNode: GenericNode<number> = { value: 123, next: null };
interface User { id: number; name: string; }
let userNode: GenericNode<User> = { value: { id: 1, name: 'Alex' }, next: null };
Sisestades tüübi parameetri `
Õudne viga: "Tüübi instantseerimine on ülemäära sügav ja võib olla lõpmatu"
Mõnikord, määratledes eriti keerulist rekursiivset tüüpi, võite kohata seda kurikuulsat TypeScripti viga. See juhtub seetõttu, et TypeScripti kompilaatoril on sisseehitatud sügavuse piir, et kaitsta end tüüpide lahendamisel lõpmatusse tsüklisse sattumise eest. Kui teie tüübi definitsioon on liiga otsene või keeruline, võib see selle piiri saavutada.
Mõelge sellele problemaatilisele näitele:
// See võib põhjustada probleeme
type BadTuple = [string, BadTuple] | [];
Kuigi see võib tunduda kehtiv, võib see viga mõnikord tekkida sellest, kuidas TypeScript tüübi aliaseid laiendab. Üks tõhusamaid viise selle lahendamiseks on kasutada `interface`-i. Liidesed loovad tüübisüsteemis nimega tüübi, millele saab viidata ilma kohese laiendamiseta, mis üldiselt käsitleb rekursiooni sujuvamalt.
// See on palju turvalisem
interface GoodTuple {
head: string;
tail: GoodTuple | null;
}
Kui peate kasutama tüübi aliast, saate mõnikord katkestada otsese rekursiooni, sisestades vahepealse tüübi või kasutades erinevat struktuuri. Kuid rusikareegel on: keerukate objektikujude puhul, eriti rekursiivsete puhul, eelistage `interface`-i `type`-i asemel.
Rekursiivsed tingimuslikud ja kaardistatud tüübid
TypeScripti tüübisüsteemi tõeline jõud vabaneb siis, kui kombineerite funktsioone. Rekursiivseid tüüpe saab kasutada täiustatud utiliidi tüüpides, näiteks kaardistatud ja tingimuslikes tüüpides, et teostada objektide struktuurides sügavaid teisendusi.
Klassikaline näide on `DeepReadonly
type DeepReadonly<T> = T extends (...args: any[]) => any
? T
: T extends object
? { readonly [P in keyof T]: DeepReadonly<T[P]> }
: T;
interface UserProfile {
id: number;
details: {
name: string;
address: {
city: string;
};
};
}
type ReadonlyUserProfile = DeepReadonly<UserProfile>;
// const profile: ReadonlyUserProfile = ...
// profile.id = 2; // Viga!
// profile.details.name = 'Uus nimi'; // Viga!
// profile.details.address.city = 'Uus linn'; // Viga!
Jaotame selle võimsa utiliidi tüübi:
- Esmalt kontrollib see, kas `T` on funktsioon, ja jätab selle nii nagu on.
- Seejärel kontrollib see, kas `T` on objekt.
- Kui see on objekt, kaardistab see iga omaduse `P` objektis `T`.
- Iga omaduse puhul rakendab see `readonly` ja seejärel - see ongi võti - see rekursiivselt kutsub `DeepReadonly`-t omaduse tüübile `T[P]`.
- Kui `T` ei ole objekt (st primitiivne), tagastab see `T` nii nagu on.
See rekursiivse tüübimanipulatsiooni muster on paljude täiustatud TypeScripti teekide jaoks fundamentaalne ja võimaldab luua uskumatult robustseid ja väljendusrikkaid utiliidi tüüpe.
Parimad tavad rekursiivsete tüüpide kasutamiseks
Rekursiivsete tüüpide tõhusaks kasutamiseks ja puhta, arusaadava koodibaasi säilitamiseks kaaluge neid parimaid tavasid:
- Eelistage liideseid avalike API-de jaoks: Rekursiivse tüübi määratlemisel, mis on osa teegi avalikust API-st või jagatud moodulist, on `interface` sageli parem valik. See käsitleb rekursiooni usaldusväärsemalt ja pakub paremaid veateateid.
- Kasutage tüübi aliaseid lihtsamate juhtumite jaoks: Lihtsate, kohalike või liidupõhiste rekursiivsete tüüpide jaoks (nagu meie `JsonValue` näide) on `type` alias täiesti vastuvõetav ja sageli kokkuvõtlikum.
- Dokumenteerige oma andmestruktuurid: Keerulist rekursiivset tüüpi võib olla raske kohe mõista. Kasutage TSDoc kommentaare, et selgitada struktuuri, selle eesmärki ja esitada näide.
- Määratlege alati baasjuhtum: Nii nagu rekursiivne funktsioon vajab baasjuhtumit oma käivitamise peatamiseks, vajab ka rekursiivne tüüp viisi lõpetamiseks. Tavaliselt on see `null`, `määratlemata` või tühi massiiv (`[]`), mis peatab iseenda viitamise ahela. Meie `LinkedListNode` puhul oli baasjuhtum `| null`.
- Kasutage diskrimineeritud liite: Kui rekursiivne struktuur võib sisaldada erinevaid sõlmede tüüpe (nagu meie `FileSystemNode` näide `File` ja `Directory` puhul), kasutage diskrimineeritud liitu. See parandab oluliselt tüübi ohutust andmetega töötamisel.
- Testige oma tüüpe ja funktsioone: Kirjutage ühiktestid funktsioonidele, mis tarbivad või toodavad rekursiivseid andmestruktuure. Veenduge, et katate äärmuslikud juhtumid, nagu tühi loend/puu, ühe sõlme struktuur ja sügavalt pesastatud struktuur.
Kokkuvõte: keerukuse omaksvõtmine elegantsiga
Rekursiivsed tüübid ei ole lihtsalt esoteeriline funktsioon teekide autoritele; need on fundamentaalne tööriist igale TypeScripti arendajale, kes peab modelleerima reaalset maailma. Alates lihtsatest loenditest kuni keerukate JSON-i puude ja domeenispetsiifiliste hierarhiliste andmeteni pakuvad iseennast viitavad definitsioonid plaani robustsete, iseenesest dokumenteerivate ja tüübikindlate rakenduste loomiseks.
Mõistes, kuidas rekursiivseid tüüpe määratleda, kasutada ja kombineerida teiste täiustatud funktsioonidega, nagu generics ja tingimuslikud tüübid, saate tõsta oma TypeScripti oskusi ja luua tarkvara, mis on nii vastupidavam kui ka kergemini põhjendatav. Järgmine kord, kui kohtate pesastatud andmestruktuuri, on teil ideaalne tööriist selle modelleerimiseks elegantsi ja täpsusega.