Search code examples
typescriptcompiler-construction

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


I am working on a compiler in TypeScript at the moment and I have an Enum to represent Token Types and a class for the actual Token:

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(',') +
            '}'
        )
    }
}

In my AST node's types I would like to specify that the Token hold a specific type, like for example in the FunctionDeclaration type:

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

I've tried using extend like:

interface IdentifierToken extends Token {
    type: TokenType.Identifier
}

However, this makes me cast a new Token(TokenType.Identifier, ...) as IdentifierToken even though the Token's type is TokenType.Identifier.

Also I would prefer to not have to declare new separate types for all the different TokenTypes (as there are ~25). So, would an inline way to enforce the values of class properties be possible?


Solution

  • You may want to consider making Token a generic class with a type parameter corresponding to the particular subtype of TokenType you are using:

    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
        }
    }
    

    Then you can easily refer to "a Token with a type equal to XXX as Token<XXX>:

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

    And additionally when you use the Token constructor, the compiler will infer T based on the construct parameters:

    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