#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
может изменяться во время выполнения, поэтому нет возможности утверждать во время компиляции, какие типы токенов используются в каких местах. Ваши варианты включают в себя:
- Переключитесь на выполнение проверок во время выполнения. Например, когда
FunctionDeclaration
создается a (или, если это обычный объект, всякий раз, когда он принимается методом), проверьте тип егоident
маркера во время выполнения. - Создайте
Token
abstract
класс иtype
abstract readonly
поле. Затем создайте подклассы для каждого типа токена (class IdentifierToken
class FloatToken
, и т.д.), Которые фиксируют его на определенном значении (readonly type = TokenType.Identifier
readonly type = TokenType.Float
, и т.д.). Обратите внимание , что это отличается от поля, доступного только для чтения, которое можно свободно назначить в аргументе конструктора. Наличие этих подклассов также может помочь позже , когда вам потребуется ввести/вывести проанализированные данные фактического типа (number
Integer
Float
например, для и). - Если у вас все равно будут подклассы, возможно, вы зададитесь вопросом, нужно ли вам
type
вообще это поле. Такого рода поля все еще могут использоваться в иерархии классов, но имейте в виду, что теперь у вас также естьinstanceof
доступные проверки, не говоря уже о простом переопределении поведения в каждом подклассе.
Так что да, в долгосрочной перспективе создание этих ~25 классов (предпочтительно реальных классов, а не просто интерфейсов) будет намного чище. Если у вас так много типов токенов, и разные типы должны вести себя по-разному в определенных контекстах, это просто необходимый уровень сложности для вашего кода.