Есть ли способ обеспечить, чтобы класс содержал определенное значение перечисления (предпочтительно встроенное)?

#typescript #compiler-construction

Вопрос:

В данный момент я работаю над компилятором в TypeScript, и у меня есть перечисление для представления типов токенов и класс для фактического токена:

 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, ...) as IdentifierToken , даже если тип токена таков TokenType.Identifier .

Кроме того, я бы предпочел не объявлять новые отдельные типы для всех различных типов токенов (так как их ~25). Итак, возможен ли встроенный способ принудительного применения значений свойств класса?

Ответ №1:

Возможно, вы захотите рассмотреть возможность создания Token универсального класса с параметром типа, соответствующим определенному подтипу 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
    }
}
 

Тогда вы можете легко сослаться на «a Token с a type , равным 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>'.
 

Ссылка на игровую площадку для кода

Комментарии:

1. Спасибо! Я не рассматривал возможность использования дженериков, но мне очень нравится эргономика этого решения!

Ответ №2:

Как определено сейчас, Token#type может изменяться во время выполнения, поэтому нет возможности утверждать во время компиляции, какие типы токенов используются в каких местах. Ваши варианты включают в себя:

  1. Переключитесь на выполнение проверок во время выполнения. Например, когда FunctionDeclaration создается a (или, если это обычный объект, всякий раз, когда он принимается методом), проверьте тип его ident маркера во время выполнения.
  2. Создайте Token abstract класс и type abstract readonly поле. Затем создайте подклассы для каждого типа токена ( class IdentifierToken class FloatToken , и т.д.), Которые фиксируют его на определенном значении ( readonly type = TokenType.Identifier readonly type = TokenType.Float , и т.д.). Обратите внимание , что это отличается от поля, доступного только для чтения, которое можно свободно назначить в аргументе конструктора. Наличие этих подклассов также может помочь позже , когда вам потребуется ввести/вывести проанализированные данные фактического типа ( number Integer Float например, для и).
  3. Если у вас все равно будут подклассы, возможно, вы зададитесь вопросом, нужно ли вам type вообще это поле. Такого рода поля все еще могут использоваться в иерархии классов, но имейте в виду, что теперь у вас также есть instanceof доступные проверки, не говоря уже о простом переопределении поведения в каждом подклассе.

Так что да, в долгосрочной перспективе создание этих ~25 классов (предпочтительно реальных классов, а не просто интерфейсов) будет намного чище. Если у вас так много типов токенов, и разные типы должны вести себя по-разному в определенных контекстах, это просто необходимый уровень сложности для вашего кода.