Я нахожусь в следующей ситуации: я реализую шаблон посетителя для прохождения AST действительно простого языка.
Здесь вы можете найти TS игровую площадку
У меня есть только четыре типа узлов, сгруппированных в различающееся объединение :
type NumericLiteral = { value: number; type: "NumericLiteral" };
type StringLiteral = { value: string; type: "StringLiteral" };
type Identifier = { name: string; type: "Identifier" };
type CallExpression = {
name: string;
arguments: DropbearNode[];
type: "CallExpression";
};
type DropbearNode =
| NumericLiteral
| StringLiteral
| Identifier
| CallExpression;
У меня также есть следующий keyof
, основанный на различающем type
:
type KeyOfDropbearNodeTypes = DropbearNode["type"];
// "NumericLiteral" | "StringLiteral" | "Identifier" | "CallExpression"
И следующая утилита, которая, учитывая type
, возвращает соответствующий узел :
type TransformDropbearNodeKeyIntoDropbearNode<
K extends KeyOfDropbearNodeTypes
> = Extract<DropbearNode, { type: K }>;
// Examples:
TransformDropbearNodeKeyIntoDropbearNode<"NumericLiteral">; // is the NumericLiteral node type
TransformDropbearNodeKeyIntoDropbearNode<"CallExpression">; // is the CallExpression node type
Благодаря этому я реализовал:
- тип Visit , который содержит некоторые информация о посещенном узле
type Visit<N extends DropbearNode = DropbearNode> = {
node: N;
parent: CallExpression | null;
};
- тип VisitorFunction , который является типом пользовательской функции, которая будет вызываться на узлах
type VisitorFunction<N extends DropbearNode> = (visit: Visit<N>) => void;
- тип VisitorObject , который содержит две функции Visitor: первая для входа в узел, вторая для выхода из
type VisitorObject<N extends DropbearNode> = {
enter: VisitorFunction<N>;
exit: VisitorFunction<N>;
};
- и, наконец, тип Visitor , который может содержать объект VisitorObject для каждого типа узла:
type Visitor = {
[K in KeyOfDropbearNodeTypes]?: VisitorObject<
TransformDropbearNodeKeyIntoDropbearNode<K>
>;
};
* 105 5 *
Таким образом, я могу создать объект следующего типа, в котором тип node
автоматически выводится в соответствии с каждым реализованным объектом VisitorObject:
const visitor: Visitor = {
CallExpression: {
enter({ node }): void {
// <-- the node type is correctly inferred as CallExpression
if (node.name === "add") {
// ...
}
},
exit({ node }): void {
// <-- the node type is correctly inferred as CallExpression
// ...
},
},
NumericLiteral: {
enter({ node }): void {}, // <-- the node type is correctly inferred as NumericLiteral
exit({ node }): void {}, // <-- the node type is correctly inferred as NumericLiteral
},
};
К сожалению, у меня проблемы с функцией traverseNode
, которая будет проходить через все AST, вызывающие функции enter
и exit
, когда это необходимо. Вот что я хотел бы написать:
const traverseNode = (currentVisit: Visit, visitor: Visitor): void => {
const { node: currentNode, parent: parentNode } = currentVisit;
visitor[currentNode.type]?.enter({
node: currentNode, // <-- error
parent: parentNode,
});
if (currentNode.type === "CallExpression") {
currentNode.arguments.forEach((node) =>
traverseNode(
{
node,
parent: currentNode,
},
visitor
)
);
}
visitor[currentNode.type]?.exit({
node: currentNode, // <-- error
parent: parentNode,
});
};
Я знаю, что если определено visitor[currentNode.type]
, currentNode
- это именно тот тип узла, который необходим для этого вызова к enter
/ exit
функция, но я не могу express это в TypeScript.
Проблема в том, что тип visitor[currentNode.type]
становится VisitorObject<NumericLiteral> | VisitorObject<StringLiteral> | VisitorObject<Identifier> | VisitorObject<CallExpression>
(undefined
может быть удален благодаря опциональной цепочке), поэтому тип узла выбирается enter
/ exit
, с точки зрения TS, должен быть в состоянии рассмотреть все такие случаи, поэтому выводится как NumericLiteral & StringLiteral & Identifier & CallExpression
.
Но это невозможно: эти типы образуют дискриминационное объединение!
Мне не удалось решить эту проблему, даже используя универсальный c:
const traverseNode = <N extends DropbearNode>(currentVisit: N, visitor: Visitor): void => {
Я покажу свое текущее решение, которое излишне избыточно:
const traverseNode = (currentVisit: Visit, visitor: Visitor): void => {
const { node: currentNode, parent: parentNode } = currentVisit;
if (currentNode.type === "Identifier") {
visitor[currentNode.type]?.enter({
node: currentNode,
parent: parentNode,
});
visitor[currentNode.type]?.exit({
node: currentNode,
parent: parentNode,
});
}
if (currentNode.type === "NumericLiteral") {
visitor[currentNode.type]?.enter({
node: currentNode,
parent: parentNode,
});
visitor[currentNode.type]?.exit({
node: currentNode,
parent: parentNode,
});
}
if (currentNode.type === "StringLiteral") {
visitor[currentNode.type]?.enter({
node: currentNode,
parent: parentNode,
});
visitor[currentNode.type]?.exit({
node: currentNode,
parent: parentNode,
});
}
if (currentNode.type === "CallExpression") {
visitor[currentNode.type]?.enter({
node: currentNode,
parent: parentNode,
});
currentNode.arguments.forEach((node) =>
traverseNode(
{
node,
parent: currentNode,
},
visitor
)
);
visitor[currentNode.type]?.enter({
node: currentNode,
parent: parentNode,
});
}
};
Я вынужден различать тип, просто чтобы понравиться TypeScript, чтобы затем выполнить ту же операцию.
Итак, как я могу реорганизовать мой код и мои типы, если это необходимо, для достижения моей цели?