I am trying to adapt an existing process that I have already written in TypeScript to be more strongly typed.
Basically, I have a tree structure that looks like this:
interface Node {
[name: string]: Leaf | Node | Node[] | undefined;
}
It is a simple tree where each node contains either a leaf node, another node, a list of nodes, or undefined, organized by property names.
I have a mechanism where rules can be run on this tree. The rules look like this:
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
}
I would like to make this more generic by specifying separate types of rules that apply to the different types of children:
optionalonly applies to children that may be undefineddataonly applies to children that are leaveslistonly applies to children that are lists (in which case it will always be true)process()only applies to children that are nodes or lists of nodes
This is what I'm envisioning:
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
}
The idea is that if I specify a rule for a specific property, the chosen rule type would would check against the type of the property.
For example, say I have a node:
const node: Node = { a: new Leaf(), b: [] };
And I process it:
process(node, [{ name: 'a', data: {} }, { b: list: true }]);
I would expect that specifying data would check somehow to make sure that node['a'] is a Leaf, and give an error if it wasn't. Likewise, the second rule would check to make sure that node['b'] is a Node[].
However, when I change data: {} to list: true on a, it gives no error, even though node['a'] is not an array. I knew I was doing something wrong from the start, but I'd like to know if something like this is even possible in TypeScript. I know that with a lot of these new features like keyof T and T[K] we are capable of describing some pretty complicated types, so I'd like to be able to achieve what I'm trying to do.
My goal is to put compile-time checks in place so that the developer can't make a mistake. Obviously none of this needs to apply at runtime because types are all stripped away.
EDIT: Basically this problem would be solved if there was a way for me to put a constraint on V = T[K] that V must be a specific type.