Folosirea TypeScript pentru a specifica reguli complexe de procesare a arborilor
Încerc să adaptez un proces existent pe care l-am scris deja în TypeScript pentru a fi mai puternic tipizat.
Practic, am o structură de copac care arată astfel:
interface Node {
[name: string]: Leaf | Node | Node[] | undefined;
}
Este un arbore simplu unde fiecare nod conține fie un nod frunză, fie un alt nod, fie o listă de noduri, fie nedefinit, organizat după numele proprietăților.
Am un mecanism în care regulile pot fi aplicate pe acest arbore. Regulile arată astfel:
interface Rule<T extends Node, K = keyof T> {
name: K;
optional?: boolean;
data?: {}; // matches against the structure of a child Leaf
list?: boolean; // whether this child should be a list
process?(processor: RuleProcessor): Node; // for processing sub-nodes
// ...other flags and stuff specific to the rule
}
Aș dori să fac acest lucru mai generic specificând tipuri separate de reguli care se aplică diferitelor tipuri de copii:
optionalse aplică doar copiilor care pot fi nedefinițidatase aplică doar copiilor care sunt frunzelistse aplică doar copiilor care sunt liste (în acest caz va fi întotdeauna adevărat)process()se aplică doar copiilor care sunt noduri sau liste de noduri
Asta este ceea ce îmi imaginez:
type Child = Leaf | Node | Node[];
interface Node {
[name: string]: Child | undefined;
}
interface Rule<T extends Node, K extends keyof T = keyof T, _V extends T[K] = T[K]> {
name: K;
}
interface OptionalRule<T extends Node, K extends keyof T = keyof T> extends Rule<T, K, Child | undefined> {
optional: true;
}
interface RequiredRule<T extends Node, K extends keyof T = keyof T, V extends Child = Child> extends Rule<T, K, V> {}
interface LeafRule<T extends Node, K extends keyof T = keyof T> extends RequiredRule<T, K, Leaf> {
data: {};
}
interface ListRule<T extends Node, K extends keyof T = keyof T> extends RequiredRule<T, K, Node[]> {
list: true;
}
interface ProcessRule<T extends Node, K extends keyof T = keyof T> extends RequiredRule<T, K, Node | Node[]> {
process(processor: RuleProcessor): Node;
}
type AnyRule<T extends Node> = OptionalRule<T> | LeafRule<T> | ListRule<T> | ProcessRule<T>;
function process<T extends Node>(node: T, rules: AnyRule<T>[]) {
// logic
}
Ideea este că dacă specific o regulă pentru o proprietate specifică, tipul de regulă ales ar verifica împotriva tipului proprietății.
De exemplu, să spun că am un nod:
const node: Node = { a: new Leaf(), b: [] };
Și îl procesez:
process(node, [{ name: 'a', data: {} }, { b: list: true }]);
M-aș aștepta ca specificarea data să verifice cumva pentru a se asigura că node['a'] este un Leaf și să dea o eroare dacă nu este. La fel, a doua regulă ar verifica pentru a se asigura că node['b'] este un Node[].
Totuși, când schimb data: {} în list: true pe a, nu apare nicio eroare, chiar dacă node['a'] nu este un array. Știam că fac ceva greșit de la început, dar aș dori să știu dacă ceva de genul acesta este măcar posibil în TypeScript. Știu că, cu multe dintre aceste funcții noi, cum ar fi keyof T și T[K], suntem capabili să descriem tipuri destul de complicate, așa că aș vrea să pot realiza ceea ce încerc să fac.
Scopul meu este să pun în aplicare verificări la timpul compilării astfel încât dezvoltatorul să nu poată face o greșeală. Evident, nimic din toate acestea nu trebuie să se aplice la rulare, deoarece tipurile sunt complet eliminate.
EDIT: Practic, această problemă ar fi rezolvată dacă ar exista o modalitate pentru mine să pun o constrângere pe V=T[K] astfel încât V să fie un tip specific.
Răspuns #1
Așadar, se pare că acest lucru este momentan rezolvabil cu tipuri mapate! Și nu este nevoie să definim tipul Node:
type Child = Leaf | Node | Node[];
interface Rule<T extends { [P in K]: V }, K extends keyof T, V> {
name: K;
}
interface OptionalRule<T extends { [P in K]?: V }, K extends keyof T, V> extends Rule<T, K, Child | undefined> {
optional: true;
}
interface RequiredRule<T extends { [P in K]: V }, K extends keyof T, V extends Child> extends Rule<T, K, V> {}
interface LeafRule<T extends { [P in K]: Leaf }, K extends keyof T> extends RequiredRule<T, K, Leaf> {
data: {};
}
interface ListRule<T extends { [P in K]: V[] }, K extends keyof T, V> extends RequiredRule<T, K, V[]> {
list: true;
}
interface ProcessRule<T extends { [P in K]: V | V[] }, K extends keyof T, V> extends RequiredRule<T, K, V | V[]> {
process(processor: RuleProcessor): V;
}
type AnyRule<T extends { [P in K]: V }, K extends keyof T, V> = OptionalRule<T, K, V> | LeafRule<T, K> | ListRule<T, K, V> | ProcessRule<T, K, V>;
function process<T extends { [P in K]: V }, K extends keyof T, V>(node: T, rules: AnyRule<T, K, V>[]) {
// logic
}
Puțin prolix, dar funcționează bine! În plus, tipurile parametrilor funcției pot fi deduse.
