Typed Get

Challenge

The get function in lodash is a quite convenient helper for accessing nested values in JavaScript. However, when we come to TypeScript, using functions like this will make you lose the type information. With TS 4.1’s upcoming - Template Literal Types feature, properly typing get becomes possible. Can you implement it?

For example,

type Data = {
  foo: {
    bar: {
      value: "foobar";
      count: 6;
    };
    included: true;
  };
  hello: "world";
};

type A = Get<Data, "hello">; // 'world'
type B = Get<Data, "foo.bar.count">; // 6
type C = Get<Data, "foo.bar">; // { value: 'foobar', count: 6 }

Accessing arrays is not required in this challenge.

Solution

Zur Lösung des Problems definieren wir einen Hilfstypen SplitStringIntoArray, der eine Zeichenkette anhand eines Separators in einen Array / Tupel aus Zeichenketten umwandelt. Beispielsweise erhalten wir bei T = foo.bar, S = . eine Tupel [foo, bar]. Dazu definieren wir zwei Akkumulatoren. Der Temp-Akkumulator speichert alle Zeichen so lange, bis wir den nächsten Separator finden. Ist dies der Fall, wird diese Zeichenkette in den Akkumulator Acc geschrieben und wieder auf einen leeren String zurückgesetzt.

// `foo.bar` => [`foo`, `bar`]
type SplitStringIntoArray<
  T extends string,
  S extends string,
  Temp extends string = "",
  Acc extends string[] = []
> = T extends `${infer H}${infer R}`
  ? H extends S
    ? SplitStringIntoArray<R, S, "", [...Acc, Temp]>
    : SplitStringIntoArray<R, S, `${Temp}${H}`, Acc>
  : [...Acc, Temp];

Für den finalen Typen laufen wir einfach über die Tupel U, und prüfen jeweils, ob die extrahierte Typvariable H eine Eigenschaft des aktuellen Objektes ist. Ansonsten wird der Typ rekursiv für das Objekt an der aktuellen Stelle aufgerufen.

type Get<
  T extends Record<string, any>,
  K extends string,
  U extends string[] = SplitStringIntoArray<K, ".">
> =
  //Prüfen des seltenen Falles, dass K eine Zeichenkette wie `foo.bar` ist.
  K extends keyof T
    ? T[K]
    : U extends [infer H, ...infer Rest extends string[]]
    ? H extends keyof T
      ? Rest["length"] extends 0
        ? T[H]
        : Get<T[H], K, Rest>
      : never
    : K;

References