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] }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).
/* _____________ 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 '../helpUnlock 150+ medium, hard, and extreme challenges to master advanced TypeScript.
Monthly subscription. Cancel anytime.
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.
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.
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.
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 neverThe 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.
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.
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.
'{}' and '[]' return {} and []. Without the empty checks in ParseLiteral these would hit the member and element helpers and fail.'[1]' and '{ 1: "world" }' return never. The digit 1 matches no branch in Tokenize, so the failure happens during tokenizing and never reaches the parser.'true' returns true. A bare keyword is a complete JSON document, so ParseLiteral has to accept one outside of any container.'{ "hello\r\n\b\f": "world" }' keeps the escapes as real control characters in the key. The Escape table is applied while tokenizing, so SetProperty receives the decoded key.never. JSON forbids unescaped control characters inside strings, and C extends ControlChar is the check that enforces it.null and a nested object inside an array. Every one of those paths goes back through ParseLiteral, so nesting depth costs nothing extra.This challenge is originally from here.
Track your progress through 100+ hands-on challenges. Free, sign in with GitHub.
Or start solving right away: explore all TypeScript challenges