#6228Extreme

JSON Parser

Parse a JSON string into an object literal type. A character-level tokenizer feeds a recursive descent parser, and invalid input has to collapse to never.

This one is a compiler, written entirely in types.

Parse<S> takes a JSON document as a string literal and returns the object literal type it describes. Numbers and \uXXXX escapes are out of scope, but everything else is in: nested objects, arrays, the three keywords, escape sequences inside strings, and rejection of anything the grammar does not allow. Invalid input has to produce never, which means the type cannot just be permissive and hope for the best.

type Result = Parse<'{ "a": "b", "c": [true, null] }'>
// expected to be { a: 'b'; c: [true, null] }

Challenge Instructions: JSON Parser

Extreme

You're required to implement a type-level partly parser to parse JSON string into a object literal type.

Requirements:

  • Numbers and Unicode escape (\uxxxx) in JSON can be ignored. You needn't to parse them.

View on GitHub: https://tsch.js.org/6228

Change the following code to make the test cases pass (no type check errors).

ChallengeSolution
/* _____________ Your Code Here _____________ */

type Pure<T> = {
  [P in keyof T]: T[P] extends object ? Pure<T[P]> : T[P]
}

type SetProperty<T, K extends PropertyKey, V> = {
  [P in keyof T | K]: P extends K ? V : P extends keyof T ? T[P] : never
}

type Token = any
type ParseResult<T, K extends Token[]> = [T, K]
type Tokenize<T extends string, S extends Token[] = []> = Token[]
type ParseLiteral<T extends Token[]> = ParseResult<any, T>

type Parse<T extends string> = Pure<ParseLiteral<Tokenize<T>>[0]>

/* _____________ Test Cases _____________ */
import type { Equal, Expect } from '../help

Pro Challenge

Unlock 150+ medium, hard, and extreme challenges to master advanced TypeScript.

Monthly subscription. Cancel anytime.

Detailed Explanation

The solution in full. Pure and SetProperty come with the starter, the rest is ours:

type Pure<T> = {
  [P in keyof T]: T[P] extends object ? Pure<T[P]> : T[P]
}
 
type SetProperty<T, K extends PropertyKey, V> = {
  [P in keyof T | K]: P extends K ? V : P extends keyof T ? T[P] : never
}
 
type Punctuation = '{' | '}' | '[' | ']' | ':' | ','
type Whitespace = ' ' | '\n' | '\r' | '\t'
type ControlChar = '\n' | '\r' | '\t' | '\b' | '\f'
 
type Escape = {
  '"': '"'
  '\\': '\\'
  '/': '/'
  b: '\b'
  f: '\f'
  n: '\n'
  r: '\r'
  t: '\t'
}
 
type Token = Punctuation | true | false | null | [value: string]
type ParseResult<T, K extends Token[]> = [T, K]
 
type TokenizeString<T extends string, Acc extends string, S extends Token[]> =
  T extends `"${infer Rest}` ? Tokenize<Rest, [...S, [Acc]]>
  : T extends `\\${infer C}${infer Rest}`
    ? C extends keyof Escape ? TokenizeString<Rest, `${Acc}${Escape[C]}`, S> : never
  : T extends `${infer C}${infer Rest}`
    ? C extends ControlChar ? never : TokenizeString<Rest, `${Acc}${C}`, S>
  : never
 
type Tokenize<T extends string, S extends Token[] = []> =
  T extends '' ? S
  : T extends `"${infer Rest}` ? TokenizeString<Rest, '', S>
  : T extends `true${infer Rest}` ? Tokenize<Rest, [...S, true]>
  : T extends `false${infer Rest}` ? Tokenize<Rest, [...S, false]>
  : T extends `null${infer Rest}` ? Tokenize<Rest, [...S, null]>
  : T extends `${infer C}${infer Rest}`
    ? C extends Whitespace ? Tokenize<Rest, S>
    : C extends Punctuation ? Tokenize<Rest, [...S, C]>
    : never
  : never
 
type ParseMember<T extends Token[], Acc> =
  T extends [[infer K extends string], ':', ...infer Rest extends Token[]]
    ? ParseLiteral<Rest> extends ParseResult<infer V, infer After extends Token[]>
      ? After extends ['}', ...infer Tail extends Token[]]
        ? ParseResult<SetProperty<Acc, K, V>, Tail>
      : After extends [',', ...infer Tail extends Token[]]
        ? ParseMember<Tail, SetProperty<Acc, K, V>>
      : never
    : never
  : never
 
type ParseElement<T extends Token[], Acc extends unknown[]> =
  ParseLiteral<T> extends ParseResult<infer V, infer After extends Token[]>
    ? After extends [']', ...infer Tail extends Token[]]
      ? ParseResult<[...Acc, V], Tail>
    : After extends [',', ...infer Tail extends Token[]]
      ? ParseElement<Tail, [...Acc, V]>
    : never
  : never
 
type ParseLiteral<T extends Token[]> =
  T extends ['{', ...infer Rest extends Token[]]
    ? Rest extends ['}', ...infer Tail extends Token[]]
      ? ParseResult<{}, Tail>
      : ParseMember<Rest, {}>
  : T extends ['[', ...infer Rest extends Token[]]
    ? Rest extends [']', ...infer Tail extends Token[]]
      ? ParseResult<[], Tail>
      : ParseElement<Rest, []>
  : T extends [infer Head, ...infer Rest extends Token[]]
    ? Head extends [infer S extends string] ? ParseResult<S, Rest>
    : Head extends true | false | null ? ParseResult<Head, Rest>
    : never
  : never
 
type Parse<T extends string> = Pure<ParseLiteral<Tokenize<T>>[0]>

It is long, but it is two small machines bolted together, and each one is boring on its own.

Why two passes

You could try to parse straight from the string, but then every rule has to cope with whitespace, and the structural rules get tangled up with character handling. Splitting the work means the tokenizer only ever looks at one character, and the parser only ever looks at one token. Tokenize turns a string into a Token[]; ParseLiteral turns a Token[] into a value plus the tokens it did not consume.

The tokenizer

Tokenize peels one piece off the front of the string and recurses on the rest, carrying the tokens found so far in S. Since types cannot mutate anything, the running output travels as an extra parameter with a default of []. That is the accumulator pattern, and [...S, C] is how it grows.

Order matters in the chain. The keyword branches are tried before the single-character branch, so `true${infer Rest}` claims the four characters at once:

// Tokenize<'[true]'> is ['[', true, ']']
// Tokenize<'{ }'>    is ['{', '}']

The final `${infer C}${infer Rest}` splits off exactly one character, because when two infer placeholders sit next to each other the first one matches a single character and the second swallows the remainder. Whitespace is dropped by recursing without touching S. Punctuation is appended. Anything else, a digit for instance, falls through to never, and that never is the whole answer: JSON numbers are out of scope, so a document containing one is simply not parseable here.

Strings need their own loop

A string token cannot be stored as a bare string, because '{' is also a string and the parser would not be able to tell a value from a brace. Wrapping it in a one-element tuple, [value: string], makes the two shapes structurally distinct while keeping the payload easy to read back out with infer.

TokenizeString walks character by character with a string accumulator and three cases: a closing quote hands control back to Tokenize, a backslash consumes the next character and looks it up in the Escape table, and anything else is appended verbatim. Unknown escapes and raw control characters both produce never:

// Tokenize<'"a\\nb"'> is [['a\nb']]
// Tokenize<'"a'>      is never

The Escape object type is doing the job a switch statement would do at runtime. C extends keyof Escape is the guard, Escape[C] is the lookup.

Recursive descent over tokens

ParseResult<T, K> is just [T, K]: the value that was parsed, and the tokens still ahead. Threading the remainder through every step is what lets a nested value stop wherever it likes and hand the rest back to its caller.

ParseLiteral dispatches on the first token. A '{' goes to ParseMember, a '[' goes to ParseElement, a wrapped string or one of the three keywords is returned immediately. Both container helpers call ParseLiteral again for each value, so the three types are mutually recursive, which is exactly how nesting gets handled without any extra machinery.

ParseMember expects the pattern [key, ':', ...rest], parses a value from rest, and then looks at what came back:

// ParseLiteral<['[', true, ',', null, ']']> is [[true, null], []]
// ParseMember<[['a'], ':', true, '}'], {}>  is [{ a: true }, []]

A '}' closes the object and returns. A ',' recurses into ParseMember, which demands another key. Because the comma branch re-enters ParseMember rather than the '{' handler, a trailing comma has nowhere to go and the type collapses to never. ParseElement mirrors this for arrays, appending to a tuple accumulator so the result keeps its element order and its exact length.

Empty containers are checked before either helper runs, since ParseMember insists on a key and ParseElement insists on a value.

The final cleanup

SetProperty builds objects one key at a time, which leaves behind a mapped type rather than a plain object literal. Pure walks the result and rebuilds it, recursing into anything that is an object, so the value the tests see is a flat literal type. It is safe to run over the non-object answers too: a homomorphic mapped type applied to a primitive gives that primitive straight back, so Pure<true> is true.

The same forgiving behaviour handles failure. never is the empty union, so ParseLiteral<never> distributes over nothing and stays never, indexing it with [0] stays never, and Pure<never> stays never. One bad character anywhere in the document propagates all the way out on its own.

Edge cases the tests cover

This challenge is originally from here.

Share this challenge

Related Challenges

Learn the Concepts

Become a TypeScript Pro

Track your progress through 100+ hands-on challenges. Free, sign in with GitHub.

Or start solving right away: explore all TypeScript challenges