Hi! I'm Luca Scalzotto
I am a passionate programmer, fanatic gamer and creative hobbyist.
Back to posts

TypeScript type time: JSON paths and leaves

Need a type-safe way to access JSON paths or object leaves in TypeScript? Look no further!

A while back I was working on a frontend project which used translations stored in JSON files. These files were loaded into a localization library, which exported a t function. This function accepted a translation key, which was a JSON path to a value in the JSON files. While using this library, we quickly ran into the issue that the t function accepted any string, and giving it an unknown translation key would simply output that key instead of a translation. Should this somehow go wrong, and an invalid key ended up in a user-facing frontend, that wouldn’t be very good for the user experience.

Ideally we’d want some sort of check which would warn us if we used an invalid translation key. Some searching later, I found just what we needed: a TypeScript type that defines a JSON path-like string from an object. Using this type instead of any string would make sure that invalid keys don’t even get past TypeScript compilation.

The full types look like this:

type Join<K, P> = K extends string
  ? P extends string
    ? `${K}${'' extends P ? '' : '.'}${P}`
    : never
  : never;

export type Paths<T> = T extends object
  ? {
    [K in keyof T]-?: K extends string
      ? `${K}` | Join<K, Paths<T[K]>>
      : never;
  }[keyof T]
  : '';

export type Leaves<T> = T extends object
  ? {
    [K in keyof T]-?: Join<K, Leaves<T[K]>>;
  }[keyof T]
  : '';

Note that the Join type is not exported because it is only used by the Paths and Leaves types, and should not be used directly.

The Leaves type is all strings that point to leaf nodes in the object, while the Paths type is all strings that point to any node in the object.

To use it:

const resources = {
  example: 'Example!',
  nested: {
    value: 'Nested value :)'
  },
  some: {
    very: {
      deep: {
        object: 'Four levels deep now'
      }
    }
  }
};

type ResourceKey = Leaves<typeof resources>;

function t(key: ResourceKey) {
  // ...
}

The result: TypeScript compilation fails if you provide an invalid value, and you get IDE autocompletion!

IDE autocompletion of a function accepting JSON-path leaves