Вынужден различать тип объединения, чтобы затем выполнить ту же операцию (Шаблон посетителя для AST) - PullRequest
1 голос
/ 09 апреля 2020

Я нахожусь в следующей ситуации: я реализую шаблон посетителя для прохождения 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, чтобы затем выполнить ту же операцию.

Итак, как я могу реорганизовать мой код и мои типы, если это необходимо, для достижения моей цели?

...