type Skip = null; interface Token { type: Type; value: Value; } type Tokens = Token[]; interface Lexer< TTokens extends Tokens, State extends Record, Methods extends Record, > { enter: ( char: string, internal: { token: >( token: TToken, ) => [TToken]; skip: Skip; state: State; methods: Methods; }, ) => TTokens | Skip; state?: State; methods?: Methods; } const empty = Object.create(null); const lexer = < const TTokens extends Tokens, State extends Record, Methods extends Record, >( lexer: Lexer, ) => { return { lexer: (source: string) => source.split("").map((char) => lexer.enter(char, { token: (token) => [token], skip: null, state: lexer.state || empty, methods: lexer.methods || empty, }), ).filter(t => !!t).flat(), }; }; const customLexer = lexer({ state: { alreadyReadChars: [] as string[], }, methods: { isNumbericChar: (char: String) => !isNaN(Number(char)), }, enter: (char, internal) => { if ( internal.state.alreadyReadChars.includes(char) || internal.methods.isNumbericChar(char) ) { return internal.skip; } internal.state.alreadyReadChars.push(char); return internal.token({ type: "char", value: { char, }, }); }, }); console.log(customLexer.lexer("abcdefg1234hijkkkk"));