Hacker Newsnew | past | comments | ask | show | jobs | submitlogin

I think I have run into a similar issue. I wrote a lexer in Typescript. It is table-based, as is the parser that runs after it. The type for the table looks something like this:

    type TokenTable<T> = {
        plus: T,
        minus: T,
        bang: T,
        parenOpen: T,
        //etc.
    };
I also have a type defined as 'type TokenID = keyof TokenTable<unknown>;' this makes it possible to check if a string is a valid key at compile-time. The innermost loop of the lexer is a for..in loop. This gives you the keys of the object. One problem: if you try to apply the TokenID type to the loop variable, you get this message: "The left-hand side of a 'for...in' statement cannot use a type annotation." Because of the design of JavaScript, TS cannot give object keys any other type but 'string', even though this type seems like a clear match.

To get the typechecking back on the keys, you either need to declare the loop variable outside of the loop itself, or use type casting like this:

    let token = "";
    let id: TokenID | undefined;
    for (let key in patterns) {
        const match = patterns[key as TokenID].exec(substring);
        if (match && (match[0].length > token.length || key == "EOF")) {
            token = match[0];
            id = key as TokenID;
        }
    }
Neither is particularly clean.


You can approximate something by defining the valid TokenTable keys as an enum, and using a mapped type for the actual TokenTable type.

There's some boilerplate in the definition, but it's fairly clean and non-repetitive. And easy to use in the "client code".

https://www.typescriptlang.org/play?#code/KYOwrgtgBAKg9ga1AS...


You can also write the array first, without using enum:

const tokens = ['parenOpen', 'bang', 'plus', 'minus'] as const;

type Token = typeof tokens[number];

type TokenTable<T> = Record<Token, T> // alias for { [key in Token]: T }

const isToken = (t: string): t is Token => tokens.includes(t);

const patterns: TokenTable<RegExp> = { bang: /\+/, // rest }


You put some real effort into the example. It's the opposite approach of how I did it, yet works just as well. Thanks!


Can you define a const array with type Array<TokenId> and use it every time you want to loop through these keys?


That's a possibility, yes. This is the only time in the program that a token table is iterated through, however. Most of the time a table is consulted to pursue an action in the parser. For example, there's a function table for when a statement is encountered, another for when an expression operand is encountered, etc. Each entry in the table is either an error message or code which completes the parsing of that statement. The awkwardness above is excusable when it is encountered so little. When writing expression-heavy stuff like 'Object.keys(typedObjName).map(...)' it's more of a problem.




Guidelines | FAQ | Lists | API | Security | Legal | Apply to YC | Contact

Search: