#151Extreme

Query String Parser

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'.

Challenge Instructions: Query String Parser

Extreme

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

Some detailed requirements:

  • Value of a key in query string can be ignored but still be parsed to true. For example, 'key' is without value, so the parser result is { key: true }.
  • Duplicated keys must be merged into one. If there are different values with the same key, values must be merged into a tuple type.
  • When a key has only one value, that value can't be wrapped into a tuple type.
  • If values with the same key appear more than once, it must be treated as once. For example, 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).

ChallengeSolution
/* _____________ 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&k

Pro Challenge

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

Monthly subscription. Cancel anytime.

Detailed Explanation

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>> }
    : never

The 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.

Tokenizing into pairs

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]

Getting the key set

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.

Collecting values with an accumulator

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.

Unwrapping the single case

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']

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