Typescript: paths of all parents of leaves

171 views Asked by At

This post is already covering how to get all paths of leaves of an object. I'm using this

type DotPrefix<T extends string> = T extends '' ? '' : `.${T}`;

/**
 * Extracts paths of all terminal properties ("leaves") of an object.
 */
export type PropertyPath<T> = (
  T extends object
    ? {
        [K in Exclude<keyof T, symbol>]: `${K}${DotPrefix<PropertyPath<T[K]>>}`;
      }[Exclude<keyof T, symbol>]
    : ''
) extends infer D
  ? Extract<D, string>
  : never;

which is based on this #66661477 answer. But now I need to pull all those paths up by one level. That is, instead of picking "album.track.id" in

interface Track {
  album: {
    track: {
      id: string
    }
  }
}

I need to pick "album.track" which is the path of the parent of the leaf "album.track.id".

How can it be done? If you know the leaves' keys, you can do this:

type ParentPath<
  T,
  P extends PropertyPath<T>,
  K extends string
> = P extends `${infer Head}.${K}` ? Head : never;

(which should be improved by constraining K to keyof ...), but what if I don't want to pass the key? The problem is, with setting K to string, Head will be inferred as the string up to the first ".".

2

There are 2 answers

0
DonFuchs On

Can you see anything wrong with this:?

export type ParentPath<T> = {
  [K in Exclude<keyof T, symbol>]: T[K] extends object
    ? `${K}${DotPrefix<ParentPath<T[K]>>}`
    : '';
}[Exclude<keyof T, symbol>] extends infer D
  ? Extract<D, string>
  : never;

It's just like PropertyPath, only is it ignoring keys of non-object-like properties...

0
wonderflame On

Arguments:

  • T - The type that we traverse
  • P extends PropertyPath<T> - The path to the leaf property
  • AccP extends string = '' - The accumulative path of already traversed properties.

The logic:

  • Check whether P is in the shape of {keyOfT}.${RestOfThePath}
    • If not, return never
    • Else if there is a . in the RestOfThePath
      • If the RestOfThePath is the property path of the T[keyOfT]
        • Call the PropertyPath recursively for the T[keyOfT] and append .${keyOfT} to the AccP
      • Return never
    • If there is not . then return ${AccP}.${keyofT}

Implementation:

type ParentPath<
  T,
  P extends PropertyPath<T>,
  AccP extends string = '',
> = P extends `${infer Head extends keyof T &
  string}.${infer Rest extends string}`
  ? Rest extends `${string}.${string}`
    ? Rest extends PropertyPath<T[Head]>
      ? ParentPath<T[Head], Rest, AccP extends '' ? Head : `${AccP}.${Head}`>
      : never
    : `${AccP}.${Head}`
  : never;

Testing:

//  "album.track"
type Test = ParentPath<Track, 'album.track.id'>;

playground