我正在尝试将已经用TypeScript编写的现有进程调整为更强类型。
基本上,我有一个树状结构,看起来像这样:
interface Node {
[name: string]: Leaf | Node | Node[] | undefined;
}
它是一个简单的树,其中每个节点都包含一个叶节点、另一个节点、一个节点列表,或者未定义的,按属性名称组织。
我有一个机制,可以在这个树上运行规则。规则如下:
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
}
我想通过指定适用于不同类型儿童的不同类型的规则来使其更通用:
optional
仅适用于可能未定义的子项data
仅适用于叶子的子代list
仅适用于作为列表的子项(在这种情况下,它将始终为true)process()
仅适用于作为节点或节点列表的子级
这就是我的设想:
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
}
其想法是,如果我为特定属性指定一个规则,那么所选的规则类型将与该属性的类型进行检查。
例如,假设我有一个节点:
const node: Node = { a: new Leaf(), b: [] };
我处理它:
process(node, [{ name: 'a', data: {} }, { b: list: true }]);
我希望指定data
会以某种方式进行检查,以确保node['a']
是Leaf
,如果不是,则会给出错误。同样,第二条规则将进行检查以确保node['b']
是Node[]
。
然而,当我在a
上将data: {}
更改为list: true
时,即使node['a']
不是数组,也不会出现错误。我从一开始就知道我做错了什么,但我想知道TypeScript中是否可能出现这样的情况。我知道有了keyof T
和T[K]
等许多新功能,我们就能够描述一些非常复杂的类型,所以我希望能够实现我想要做的事情
我的目标是设置编译时检查,这样开发人员就不会犯错误。显然,这些都不需要在运行时应用,因为类型都被剥离了。
编辑:基本上,如果有一种方法让我对V = T[K]
施加约束,即V
必须是特定类型,这个问题就会得到解决。
因此,事实证明,这目前可以通过映射类型来解决!并且无需定义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
}
有点冗长,但效果很好!可以推断出加号函数类型参数。