Hone logo
Hone
Problems

Type-Level Parser Combinators in TypeScript

This challenge explores the fascinating world of type-level programming in TypeScript. You'll be building parser combinators – functions that take other parser combinators as input and combine them to create more complex parsers – operating entirely at the type level. This allows you to define the grammar of a language directly in your types, enabling compile-time validation and powerful type-safe code generation.

Problem Description

The goal is to implement a set of basic parser combinators using TypeScript's advanced type system. These combinators will allow you to define and compose parsers that operate on a string, ultimately extracting structured data represented by TypeScript types. You'll be working with conditional types, mapped types, and potentially recursive types to achieve this. The parsers themselves don't execute; instead, they describe the expected structure, and the type system enforces that structure.

Specifically, you need to implement the following combinators:

  • char(c: string): Parses a single character c. The parser's type will be a conditional type that checks if the input string starts with c.
  • string(s: string): Parses a specific string s. Similar to char, but for a sequence of characters.
  • seq<P1, P2, R = P1 extends infer A ? P2 extends infer B ? A & B : never : never>: Sequentially parses two parsers P1 and P2. The resulting parser's type will be the intersection of the types produced by P1 and P2. The R type is a default type that combines the results of P1 and P2.
  • alt<P1, P2, R = P1 extends infer A ? P2 extends infer B ? A | B : never : never>: Alternates between two parsers P1 and P2. The resulting parser's type will be the union of the types produced by P1 and P2. The R type is a default type that combines the results of P1 and P2.
  • many<P, R = P[]>: Parses zero or more occurrences of parser P. The resulting parser's type will be an array of the type produced by P. The R type is a default type that represents an array of the parser's result.

The input to your parsers will be a string. The output of your parsers will be a TypeScript type representing the parsed data. The type system will be your primary tool for validation.

Examples

Example 1:

// Define parsers for 'a' and 'b'
const parseA = char('a');
const parseB = char('b');

// Combine them sequentially
type Parsed = typeof seq(parseA, parseB); // Expected: "a" & "b" which resolves to never because the types are strings.

// This demonstrates that the parser *describes* the structure, not executes.

Example 2:

// Define parsers for 'a' and 'b'
const parseA = char('a');
const parseB = char('b');

// Alternate between them
type Parsed = typeof alt(parseA, parseB); // Expected: "a" | "b"

Example 3:

// Define a parser for a digit
const parseDigit = char('0') | char('1') | char('2') | char('3') | char('4') | char('5') | char('6') | char('7') | char('8') | char('9');

// Parse many digits
type ParsedNumber = typeof many(parseDigit); // Expected: string[]

// This represents a string of digits.  The actual parsing would happen elsewhere,
// but the type system enforces the structure.

Constraints

  • All combinators must be implemented using TypeScript's type system (conditional types, mapped types, etc.). No runtime code execution is allowed within the combinators themselves.
  • The seq and alt combinators should include default types for R to handle cases where the input parsers don't explicitly provide one.
  • The many combinator should produce an array type.
  • The combinators should be generic, allowing them to work with different parser types.
  • The code should be well-typed and readable.

Notes

  • Think about how to use conditional types to check if a string starts with a specific character or sequence of characters.
  • Consider how to represent the "remaining" string after a parser has successfully parsed a portion of the input. While you won't be explicitly manipulating the string within the type system, understanding this concept is crucial for designing more complex parsers.
  • This is a challenging problem that requires a good understanding of TypeScript's advanced type system. Start with the simpler combinators (char, string) and gradually work your way up to the more complex ones (seq, alt, many).
  • The goal is to define the structure of the parser at the type level. Actual parsing logic (e.g., consuming characters from a string) is outside the scope of this challenge.
  • Focus on the type definitions of the combinators. The actual implementation is purely type-level.
Loading editor...
typescript