0

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

DonFuchs
  • 371
  • 2
  • 14

2 Answers2

0

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

DonFuchs
  • 371
  • 2
  • 14
0

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

wonderflame
  • 6,759
  • 1
  • 1
  • 17