有没有办法强制 class 包含特定的枚举值(最好是内联的)?

Is there a way to enforce that a class contains a specific Enum value (preferably inline)?

我目前正在使用 TypeScript 编写编译器,我有一个枚举来表示令牌类型,class 用于实际令牌:

enum TokenType {
    String,
    Integer,
    Float,
    Identifier,
    // ... elided
}

class Token {
    type: TokenType
    lexeme: string
    lineNo: number
    columnNo: number

    constructor(
        type: TokenType,
        lexeme: string,
        lineNo: number,
        columnNo: number
    ) {
        this.type = type
        this.lexeme = lexeme
        this.lineNo = lineNo
        this.columnNo = columnNo
    }

    toString(): string {
        return (
            'Token{' +
            [this.type, this.lexeme, this.lineNo, this.columnNo].join(',') +
            '}'
        )
    }
}

在我的 AST 节点类型中,我想指定令牌持有特定类型,例如在 FunctionDeclaration 类型中:

type FunctionDeclaration = {
    ident: Token with type = TokenType.identifier
    //           ^ Imaginary syntax, but this is what I'm trying to do
}

我试过使用 extend 比如:

interface IdentifierToken extends Token {
    type: TokenType.Identifier
}

但是,这让我将 new Token(TokenType.Identifier, ...) 转换为 IdentifierToken,即使令牌的类型是 TokenType.Identifier.

此外,我宁愿不必为所有不同的 TokenTypes 声明新的单独类型(因为有 ~25 个)。那么,是否可以采用内联方式强制执行 class 属性的值?

按照现在的定义,Token#type 可以在 运行 时发生变化,因此无法在编译时断言在哪些地方使用了哪种令牌类型。您的选择包括:

  1. 切换到进行运行时间检查。例如,当构造 FunctionDeclaration 时(或者,如果它是一个普通对象,只要它被方法接受),在 运行 时间检查其 ident 标记的类型。
  2. Token 设为 abstract class 并将 type 设为 abstract readonly 字段。然后为每个标记类型(class IdentifierTokenclass FloatToken 等)创建 subclasses,将其固定为特定值(readonly type = TokenType.Identifierreadonly type = TokenType.Float 等) .).请注意,这与可以在构造函数参数中自由分配的非只读字段不同。当您需要 input/output 解析实际类型的数据时(例如 IntegerFloatnumber),拥有这些子classes 也可能会有所帮助.
  3. 如果您无论如何都要有 subclasses,也许要问您是否需要 type 字段。这种字段仍然可以在 class 层次结构中使用,但请记住,您现在还可以使用 instanceof 检查,更不用说每个子 class 中的简单覆盖行为了。

所以是的,在漫长的 运行 中,制作那些 ~25 classes(最好是真实的 classes 而不仅仅是接口)将会变得更加清晰。如果您有那么多令牌类型,并且不同类型需要在特定上下文中表现不同,那么这就是您的代码的必要复杂程度。

您可能需要考虑将 Token 设为 generic class,其类型参数对应于您正在使用的 TokenType 的特定子类型:

class Token<T extends TokenType = TokenType> {
    type: T
    lexeme: string
    lineNo: number
    columnNo: number

    constructor(
        type: T,
        lexeme: string,
        lineNo: number,
        columnNo: number
    ) {
        this.type = type
        this.lexeme = lexeme
        this.lineNo = lineNo
        this.columnNo = columnNo
    }
}

然后你可以很容易地将“Tokentype 等于 XXX 称为 Token<XXX>:

type FunctionDeclaration = {
    ident: Token<TokenType.Identifier>
}

此外,当您使用 Token 构造函数时,编译器将根据构造参数推断 T

const identifierToken = new Token(TokenType.Identifier, "", 1, 2);
// const identifierToken: Token<TokenType.Identifier>

const f: FunctionDeclaration = { ident: identifierToken }; // okay

const floatToken = new Token(TokenType.Float, "", 3, 4);
// const floatToken: Token<TokenType.Float>

const g: FunctionDeclaration = { ident: floatToken }; // error!
// Type 'Token<TokenType.Float>' is not assignable to type 'Token<TokenType.Identifier>'.

Playground link to code