Parse a URL query string into an object type. Bare keys become true, repeated values are deduplicated, and a key with one value keeps that value unwrapped.
ParseQueryString<S> turns a URL query string into an object literal type. Splitting on & and = is the easy half; the merging rules are what make this an extreme.
type Result = ParseQueryString<'k1=v1&k2=v2&k1=v2'>
// expected to be { k1: ['v1', 'v2']; k2: 'v2' }Four rules govern the output. A key written without a value parses to true. Duplicated keys merge into one property. If a key carries several distinct values, they become a tuple in the order they appeared. If it carries only one, that value is stored bare, not in a one-element tuple. And a value repeated under the same key counts once, so 'k=v&k=v' is the same as 'k=v'.
You're required to implement a type-level parser to parse URL query string into a object literal type.
Some detailed requirements:
true. For example, 'key' is without value, so the parser result is { key: true }.key=value&key=value must be treated as key=value only.View on GitHub: https://tsch.js.org/151
Change the following code to make the test cases pass (no type check errors).
/* _____________ Your Code Here _____________ */
type ParseQueryString<S extends string> = any
/* _____________ Test Cases _____________ */
import type { Equal, Expect } from '../helpers'
type cases = [
Expect<Equal<ParseQueryString<''>, {}>>,
Expect<Equal<ParseQueryString<'k1'>, { k1: true }>>,
Expect<Equal<ParseQueryString<'k1&k1'>, { k1: true }>>,
Expect<Equal<ParseQueryString<'k1&k2'>, { k1: true; k2: true }>>,
Expect<Equal<ParseQueryString<'k1=v1'>, { k1: 'v1' }>>,
Expect<Equal<ParseQueryString<'k1=v1&k1=v2'>, { k1: ['v1', 'v2'] }>>,
Expect<Equal<ParseQueryString<'k1=v1&kUnlock 150+ medium, hard, and extreme challenges to master advanced TypeScript.
Monthly subscription. Cancel anytime.
The solution in full:
type Pair = [key: string, value: string | true]
type ParsePair<S extends string> = S extends `${infer K}=${infer V}`
? [K, V]
: [S, true]
type ParsePairs<S extends string> = S extends `${infer Head}&${infer Rest}`
? [ParsePair<Head>, ...ParsePairs<Rest>]
: [ParsePair<S>]
type IsSame<A, B> = [A] extends [B] ? ([B] extends [A] ? true : false) : false
type Includes<T extends unknown[], U> = T extends [infer Head, ...infer Rest]
? IsSame<Head, U> extends true
? true
: Includes<Rest, U>
: false
type ValuesOf<
P extends Pair[],
K extends string,
Acc extends unknown[] = [],
> = P extends [infer Head extends Pair, ...infer Rest extends Pair[]]
? ValuesOf<
Rest,
K,
Head[0] extends K
? Includes<Acc, Head[1]> extends true
? Acc
: [...Acc, Head[1]]
: Acc
>
: Acc
type Unwrap<T extends unknown[]> = T extends [infer Only] ? Only : T
type ParseQueryString<S extends string> = S extends ''
? {}
: ParsePairs<S> extends infer P extends Pair[]
? { [K in P[number][0]]: Unwrap<ValuesOf<P, K>> }
: neverThe shape of it is worth naming before the details: tokenize the string into a flat list of pairs, then build the object one key at a time by walking that list again. Trying to merge while tokenizing is where this challenge usually goes wrong, because the merge rule for a key depends on values that may still be ahead in the string.
ParsePair looks for a single =. Template literal inference is lazy, so K matches the shortest prefix it can and everything after the first = lands in V. No = means the key stood alone, and the challenge says that is true:
// ParsePair<'k1=v1'> is ['k1', 'v1']
// ParsePair<'k1'> is ['k1', true]ParsePairs does the same trick with &, and spreads the recursive result into a tuple. The spread is what keeps the output flat instead of nesting one level per separator:
[object Object]P[number] indexes the tuple with the number type, which gives the union of all its elements. Indexing that with 0 picks the first slot of each:
[object Object]A union of literal keys is exactly what a mapped type wants. Duplicate keys collapse for free here, because 'k1' | 'k1' is just 'k1'. That is the whole answer to rule two, and it costs nothing.
The ParsePairs<S> extends infer P extends Pair[] line is not doing any logic. It names the parsed tuple once so the mapped type can use it twice without recomputing it, and it re-states the constraint so P can be handed to ValuesOf.
ValuesOf walks the pair list front to back carrying Acc, a tuple it grows as it goes. Recursive types cannot mutate anything, so the running result travels as an extra type parameter with a default of []. Pairs whose key does not match are skipped by passing Acc through unchanged; matching pairs are appended with [...Acc, Head[1]], but only when Includes says the value is not there yet. That single guard implements rule four, and because appends happen in list order, the tuple comes out in appearance order.
// ValuesOf<[['k1', 'v1'], ['k1', true]], 'k1'> is ['v1', true]
// ValuesOf<[['k1', 'v1'], ['k2', 'v2'], ['k1', 'v2']], 'k2'> is ['v2']IsSame compares two types by checking that each extends the other. The square brackets around A and B matter: a naked type parameter on the left of extends distributes over unions, so A extends B with A = 'v1' | 'v2' would be evaluated once per member and the answers joined. Wrapping both sides in one-element tuples turns the check into a single comparison of whole types.
Everything above always produces a tuple, so rule three is a last step rather than a special case threaded through the recursion. Unwrap matches a tuple of exactly one element and returns that element; anything longer falls through untouched:
// Unwrap<['v1']> is 'v1'
// Unwrap<['v1', 'v2']> is ['v1', 'v2']'' is intercepted before parsing. Without the guard, ParsePairs<''> returns [['', true]] and you would get { '': true } instead of {}.'k1&k1' gives { k1: true }. Both pairs carry the value true, Includes rejects the second, and Unwrap strips the one-element tuple.'k1=v1&k1' gives { k1: ['v1', true] }. Mixed value kinds live in the same tuple, and IsSame<'v1', true> is false, so nothing is deduplicated.'k1=v1&k1=v2&k1=v1' gives { k1: ['v1', 'v2'] }. The third pair repeats a value already collected, so the tuple stays at two entries and never gets unwrapped.'k1=v1&k2=v2&k1=v2' gives { k1: ['v1', 'v2']; k2: 'v2' }. The same value 'v2' appears under two different keys, which is fine: deduplication is scoped per key, since ValuesOf starts a fresh accumulator for each one.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