Query String Parser

Challenge

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.

Solution

Zur Lösung dieser Challenge definieren wir zwei Hilfstypen Merge, welches zwei Objekte zusammenführt, so ParsePair, welcher Query-Parameter in einen Objekttyp umwandelt:

Kombiniert zwei Objekte zu einem Objekt. Sollte ein Attribut in beiden Objekten vorkommen, so wird ein Array mit allen Werten zurückgegeben.
// Merge<{foo:2}, {foo:3}> => {foo: [2,3]}
// Merge<{foo:2}, {gg: 4}> => {foo: 2, gg:4}
type Merge<T, U> = {
  [K in keyof T | keyof U]: K extends keyof U
    ? K extends keyof T
      ? U[K] extends unknown[]
        ? [T[K], ...U[K]]
        : T[K] extends U[K]
        ? T[K]
        : [T[K], U[K]]
      : U[K]
    : K extends keyof T
    ? T[K]
    : never
}
Wandelt die Query Parameter in Form von "T=U" in ein Objekt um. Fehlt der Wert (z.b. bei `k1`), so wird true als Wert zum Attribut zurückgegeben.
// 'k1=4' => { k1: 4}
// 'k1'   => { k1: true}
type ParsePair<T extends string> = T extends `${infer K}=${infer V}`
  ? {
      [P in K]: V;
    }
  : {
      [P in T]: true;
    };

Mit diesen beiden Hilfstypen muss man nun noch über die Zeichenkette mittels infer laufen, und die Werte erst in Objekttypen umwandeln, und anschließend zusammenführen:

type ParseQueryString<T extends string> = T extends ""
  ? // Base-Case: Bei einem leeren String wird ein leerer Object-Typ returnt
    {}
  : T extends `${infer P}&${infer R}`
  ? //Finden wir einen Query-Parameter, so wandeln wir diesen in ein Objekt um, und mergen dieses Objekt mit allen weiteren Objekten, die durch den rekursiven Aufruf noch gefunden werden
    Merge<ParsePair<P>, ParseQueryString<R>>
  : ParsePair<T>;

References