07258-hard-object-key-paths

Back

This one works on recursive/circular objects. For arrays, it only accepts .0, for cleaner IDE suggestions, instead of things like .[$number], but you can change it yourself if you want to use that notation.

type Digit = 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9
type NextDigit = [1, 2, 3, 4, 5, 6, 7, 'STOP']
type Inc<T> = T extends Digit ? NextDigit[T] : 'STOP'
type StringOrNumKeys<TObj> = TObj extends unknown[] ? 0 : keyof TObj & string
type NestedPath<TValue, Prefix extends string, TValueNestedChild, TDepth> = TValue extends object
    ? `${Prefix}.${TDepth extends 'STOP' ? string : NestedFieldPaths<TValue, TValueNestedChild, TDepth>}`
    : never
type GetValue<T, K extends string | number> = T extends unknown[]
    ? K extends number
        ? T[K]
        : never
    : K extends keyof T
      ? T[K]
      : never
type NestedFieldPaths<TData = any, TValue = any, TDepth = 0> = {
    [TKey in StringOrNumKeys<TData>]:
        | (GetValue<TData, TKey> extends TValue ? `${TKey}` : never)
        | NestedPath<GetValue<TData, TKey>, `${TKey}`, TValue, Inc<TDepth>>
}[StringOrNumKeys<TData>]
export type ObjectKeyPaths <TData = any> = TData extends any ? NestedFieldPaths<TData, any, 1> : never

Credits: I based the code on the article Supporting Circularly Referenced Mapped Types in Typescript, by Stephen Cooper

>>> TS Playground <<<

Solution by mauriciabad #34401

type ObjectKeyPaths<
  T extends object,
  K extends keyof T = keyof T
> = K extends any
  ? T[K] extends any[]
    ? T[K][number] extends object
      ?
          | K
          | `${K & string}${
              | `.${ArrayIndex<T[K]>}.`
              | `[${ArrayIndex<T[K]>}]`}${ObjectKeyPaths<T[K][number]> &
              string}`
      : K | `${K & string}${`.${T[K][number]}` | `[${T[K][number]}]`}`
    : T[K] extends object
    ? K | `${K & string}.${ObjectKeyPaths<T[K]> & string}`
    : K
  : never;
type ArrayIndex<T extends any[]> = {
  [P in keyof T]: P
}[number]

Solution by Vampirelee #32650

type ObjectKeyPaths<
  T,
  D extends string = "",
  K extends keyof T = keyof T
> = K extends string | number
  ? `${`${D}${K}` | (T extends unknown[] ? `[${K}]` | `${D}[${K}]` : never)}${
      | ""
      | (T[K] extends object ? ObjectKeyPaths<T[K], "."> : "")}`
  : never;

Solution by vangie #32311

type ObjectKeyPaths<T extends Record<string, any>, K extends keyof T = keyof T> = K extends keyof T ? K extends string | number ? T[K] extends Record<string, any> ? T[K] extends unknown[] ? ${K} | ${K}[${number}] | ${K}.[${number}] | ${K}.${ObjectKeyPaths<T[K]>} : ${K} | ${K}.${ObjectKeyPaths<T[K]>} : ${K} : never : never

Solution by Czhongda #30857

// your answers
type AddPath<Keys, Path> = Keys extends string | number ? Path extends string | number ? Keys extends number ? `${Path}.${Keys}` | `${Path}[${Keys}]` | `${Path}.[${Keys}]`    : `${Path}.${Keys}` : never : never

type GetObject<T extends object> = {
  [Key in keyof T as T[Key] extends object ? AddPath<keyof GetObject<T[Key]>, Key> | Key : Key]: 1
}

type ObjectKeyPaths<T extends object> = keyof GetObject<T>

Solution by 437204933 #29718

/**是否为readonly数组 */
type IsReadonlyArray<T extends readonly any[]> = [T[`length`] & -1] extends [never] ? true : false;
/**固定将keyof输出成string */
type ToKeyString<K extends PropertyKey> = K extends string | number ? `${K}` : ``;
/**取[index].type的字符串形式 */
type ToIndexTypeString<S extends readonly any[]> = ToKeyString<keyof { [P in keyof S as
  IsReadonlyArray<S> extends true ?   //应付as const情景
  (P extends `${number}` ?
    `${(`${`` | `.`}[${ToKeyString<P>}]`) | `.${ToKeyString<P>}`}${S[P] extends object ? `.${ObjectKeyPaths<S[P]>}` : ``}` :    //[0] .[0] .0 (.type)
    ``)
  :
  (`${(`${`` | `.`}[${number}]`) | `.${number}`}${S[number] extends object ? `.${ObjectKeyPaths<S[number]>}` : ``}`)    //[${number}] .[${number}] .${number} (.type)
  ]: any }>;

//数组情况分两种:
//1. ref{} as const 时,数组为readonly,索引数可以确定,例:person.books[1]、person.pets.0.type
//2. ref{} 不 as const 时,数组不为readonly,索引数不可确定,因此转化为${number},例:person.books[${number}]、person.pets.${number}.type
//此处题目为第二种情景
type ObjectKeyPaths<T extends object> = ToKeyString<
  keyof T | keyof { [P in keyof T as
    (P extends string | number ?
      T[P] extends readonly unknown[] ? `${P}${ToIndexTypeString<T[P]>}` :    //数组
      (T[P] extends object ? `${P}.${ObjectKeyPaths<T[P]>}` : P) :            //结构体
      P)]: T[P] }
>;

Solution by E-uler #25015

type JoinPrefix<Prefix extends string, Path extends string> = Prefix extends '' ? Path : `${Prefix}.${Path}`

type Num2Array<N extends number, T extends unknown[] = []> = T['length'] extends N ? T : Num2Array<N, [...T, unknown]>
type AddOne<N extends number> = [...Num2Array<N>, unknown]['length']

/**
 * 注意: 数组类型是object的子类型,所以在高级类型ObjectKeyPaths中要先判断数组的情况
 */
type ArrayIsExtendsObject = [1, 2, 3] extends object ? true : false

type ObjectKeyPaths<T extends object, Prefix extends string = '', Key extends keyof T = keyof T> = Key extends Key ?
    JoinPrefix<Prefix, Key & string> | (T[Key] extends any[] ? ArrayKeyPath<T[Key], never, JoinPrefix<Prefix, Key & string>> :
        T[Key] extends object ? ObjectKeyPaths<T[Key], JoinPrefix<Prefix, Key & string>> : never) : never

type ArrayKeyPath<T extends any[], Res = never, Prefix extends string = '', Index extends number = 0> = T extends [infer Cur, ...infer Rest] ?
    ArrayKeyPath<Rest, Res | JoinPrefix<Prefix, `${Index}`> |
        (Cur extends any[] ? ArrayKeyPath<Cur, never, JoinPrefix<Prefix, `${Index}`>> :
            Cur extends object ? ObjectKeyPaths<Cur, JoinPrefix<Prefix, `${Index}`>> : never)
        , Prefix, AddOne<Index> & number> : Res

Solution by zhuizhui0429 #24990

type ConcatPath<P extends string | number, K extends string | number> =
  [P] extends [never]
    ? K
    : `${P}.${K}` | ([K] extends [number] ? `${P}[${K}]` | `${P}.[${K}]` : never)

type ObjectKeyPaths<
  T,
  P extends string | number = never,
  K extends keyof T = keyof T,
> =
  T extends Record<string, unknown>[]
    ? ConcatPath<P, number> | ObjectKeyPaths<T[number], ConcatPath<P, number>>
    : T extends unknown[]
      ? ConcatPath<P, number>
      : T extends Record<string, unknown>
        ? K extends string | number
          ? ConcatPath<P, K> | ObjectKeyPaths<T[K], ConcatPath<P, K>>
          : never
        : never

Solution by drylint #23300

// your answers
type BuildArray<
    Length extends number, 
    Ele = unknown, 
    Arr extends unknown[] = []
> = Arr['length'] extends Length 
    ? Arr 
    : BuildArray<Length, Ele, [...Arr, Ele]>;
        
type Add<Num1 extends number, Num2 extends number> = 
    [...BuildArray<Num1>, ...BuildArray<Num2>]['length'];

type GetObjectKeyPathsByArr<T extends any[], Index extends number = 0> =
 T extends [infer F, ...infer L]
     ? (F extends Record<PropertyKey, any> ? `[${Index}]` | `[${Index}].${keyof F & string}`
     | GetObjectKeyPathsByArr<L, Add<Index, 1>>
         : `[${Index}]` | GetObjectKeyPathsByArr<L, Add<Index, 1>>) : `${T[number]}_`

type ObjectKeyPaths<T extends object> = {
    [key in keyof T]:
    T[key] extends any[] ? key | `${key & string}.${GetObjectKeyPathsByArr<T[key], 0> & string}` :
        (T[key] extends Record<PropertyKey, any> ? key | `${key & string}.${ObjectKeyPaths<T[key]> & string}` :
            key)
}[keyof T]

Solution by xinxinhe1810 #22951

Considering that so far the best solution was this, current one is very close to it, but a little bit more compact

type Keys<O, IsTop, K extends string | number> =
  IsTop extends true
    ? K | (O extends unknown[] ? `[${K}]` : never)
    : `.${K}` | (O extends unknown[] ? `[${K}]` | `.[${K}]` : never)

type ObjectKeyPaths<T, IsTop = true, K extends keyof T = keyof T> =
  K extends string | number
    ? `${Keys<T, IsTop, K>}${'' | (T[K] extends object ? ObjectKeyPaths<T[K], false> : '')}`
    : never

Solution by Alexsey #22298

// your answers
type ObjectKeyPaths<
	T extends object,
	R extends string = '',
	K extends keyof T = keyof T
> = K extends K
	? T[K] extends Record<keyof any, any>
		?
				| (R extends ''
						? K
						: K extends number
						? `${R}.${K}` | `${R}.[${K}]` | `${R}[${K}]`
						: `${R}.${K & string}`)
				| ObjectKeyPaths<
						T[K],
						R extends ''
							? `${K & (string | number)}`
							: K extends number
							? `${R}.${K}` | `${R}.[${K}]` | `${R}[${K}]`
							: `${R}.${K & string}`
				  >
		: R extends ''
		? K
		: K extends number
		? `${R}.${K}` | `${R}.[${K}]` | `${R}[${K}]`
		: `${R}.${K & string}`
	: never

Solution by TKBnice #22291

type ObjectKeyPaths<T, PropertyBasePath extends string = '', Key extends keyof T = keyof T> = Key extends
  | string
  | number
  ?
      | `${PropertyBasePath}${Key}`
      | (T extends any[] ? `${PropertyBasePath}[${Key}]` : never)
      | (T[Key] extends object
          ? ObjectKeyPaths<T[Key], `${PropertyBasePath}${Key}.`>
          : never)
  : never;

Solution by zqiangxu #22088

// 这该死的题浪费了我两天时间
// ArrIndex  ... 题目里不是元组,所以没法指定下标.直接 number 完事, 然后 T[number] 遍历它的类型,
type ArrIndex = `[${number}]` | `.[${number}]` | `.${number}`
type ObjectKeyPaths<
    T extends object,
    P extends string = '',
    K extends keyof T = keyof T
> = K extends keyof T
    ? `${P}${K & string}`
    | (
        T[K] extends any[]
        ? `${P}${K & string}${ArrIndex}`
// T[K][number]  这样就能把 动态数组里的类型全给遍历出来了,
        | ObjectKeyPaths<T[K][number], `${P}${K & string}${ArrIndex}.`>
        : T[K] extends object
        ? ObjectKeyPaths<T[K], `${P}${K & string}.`>
        : never
    )
    : never

Solution by goddnsgit #22011

There is a bug in current playground, we need to add as const to the ref, otherwise the typeof ref will result in incorrect type.

image
type WithPrefix<K extends string, P extends string = ''> = P extends '' ? K : `${P}.${K}`

// see https://github.com/microsoft/TypeScript/issues/32917 idea 2
// with some modification to adapt them to this challenge
type ArrayKeys = Exclude<keyof [], symbol | number>
type Indices<T> = Exclude<keyof T, ArrayKeys | symbol | number>;

type Tuple2Object<T extends readonly unknown[]> = {
  [P in Indices<T>]: T[P]
}

type ExtraTupleKeysOfIndices<T extends readonly unknown[], P extends string, _I = Indices<T>> =
  _I extends string
  ? `${P}[${_I}]` | `${P}.[${_I}]`
  : never

type ExtraTupleKeys<T extends readonly unknown[], P extends string = ''> = WithPrefix<ArrayKeys, P> | ExtraTupleKeysOfIndices<T, P>

type ObjectKeyPaths<T extends Record<string, unknown>, P extends string = '', _K extends Extract<keyof T, string> = Extract<keyof T, string>> = 
  _K  extends unknown
  ? T[_K] extends Record<string, unknown>
    ? WithPrefix<_K, P> | ObjectKeyPaths<T[_K], WithPrefix<_K, P>>
    : T[_K] extends readonly unknown[]
      ? WithPrefix<_K, P> | ExtraTupleKeys<T[_K], WithPrefix<_K, P>> | ObjectKeyPaths<Tuple2Object<T[_K]>, WithPrefix<_K, P>>
      : WithPrefix<_K, P>
  : never

playground

The core part is not hard, but this challenge has many edge case need to be handled carefully.

For me, the most challenging part is how to get the indices of a tuple. I am inspired by this issue's idea 2, which gives me the following type utility:

type Indices<T> = Exclude<keyof T, keyof []>;

Solution by zhaoyao91 #21998

type ExcludeArrayKey<T> = Exclude<keyof T & string, keyof Array<any> & string>;
type ObjectKeyPaths1<
  T extends object,
  G extends keyof T = keyof T
> = G extends string
  ? T[G] extends readonly any[]
    ?
        | `${G}${
            | `[${ExcludeArrayKey<T[G]>}]`
            | `.${ObjectKeyPaths1<T[G]>}`
            | `.[${ObjectKeyPaths1<T[G]>}]`}`
        // | `${G}.${keyof T[G] & string}`
    : T[G] extends Record<string, any>
    ? `${G}${`.${ObjectKeyPaths1<T[G]>}`}`
     | `${G}.${keyof T[G] & string}`
    : G
  : never;
type ObjectKeyPaths<T extends object> = ObjectKeyPaths1<T> | keyof T;

Solution by so11y #21292

It seems similar to #18285

type _Prefix<K, Root extends boolean = true> = K extends number
  ? `.${ K }` | `[${ K }]` | `.[${ K }]`
  : K extends string
    ? `${ Root extends true ? '' : '.' }${ K }`
    : never

type ObjectKeyPaths<
  T extends object,
  Root extends boolean = true,
  _KOT extends keyof T = keyof T,
> = _KOT extends unknown
  ? _Prefix<_KOT, Root> | (
    T[_KOT] extends object
      ? `${ _Prefix<_KOT, Root> extends string ? _Prefix<_KOT, Root> : '' }${ ObjectKeyPaths<T[_KOT], false> }`
      : never
  )
  : never

Solution by lvjiaxuan #20576

type ObjectKeyPaths<T extends unknown, K extends keyof T = keyof T> = T extends
  | Record<any, unknown>
  | unknown[]
  ? K extends keyof T & (string | number)
    ? `${
        // if T is an array, K could be in []
        T extends unknown[] ? `[${K}]` | K : K
      }${
        | ''
        | `${
            // if T[K] is an array, there could be a empty string between T and T[K]
            T[K] extends unknown[] ? '' | '.' : '.'
          }${ObjectKeyPaths<T[K]>}`}`
    : never
  : never

Solution by theoolee #19758

// ❌ todo:这个还没搞懂

type GetPath<K extends PropertyKey & (string | number), Prefix extends string = ''> = [Prefix] extends [never]
  ? `${K}`
  : K extends number
  ? `${Prefix}.${K}` | `${Prefix}[${K}]` | `${Prefix}.[${K}]`
  : `${Prefix}.${K}`;

type ObjectKeyPaths<T extends object, Result extends string = never> =
  | Result
  | {
      [p in keyof T & (string | number)]: T[p] extends object ? ObjectKeyPaths<T[p], GetPath<p, Result>> : GetPath<p, Result>;
    }[keyof T & (string | number)];

Solution by CaoXueLiang #19509

// your answers

type GetPath<K extends PropertyKey & (string | number), Prefix extends string = ''> = [Prefix] extends [never]
  ? `${K}`
  : K extends number
    ? `${Prefix}.${K}` | `${Prefix}[${K}]` | `${Prefix}.[${K}]`
    : `${Prefix}.${K}`

type ObjectKeyPaths<T extends object, Result extends string = never> = Result | {
  [P in keyof T & (string | number)]: T[P] extends object
    ? ObjectKeyPaths<T[P], GetPath<P, Result>>
    : GetPath<P, Result>
}[keyof T & (string | number)]

Solution by humandetail #16505

// your answerstype ObjectKeyPaths<
    T,
    O extends any =T ,
    Keys extends (string|number)&keyof T = Exclude<(string|number)&keyof T,string&keyof []>> =
    T extends object
    ? Keys extends keyof O
    ? `${Keys}`
    | `${Keys}${ObjectKeyPaths<T[Keys],O>}`
    : Keys extends string
    ? `.${Keys}`
    | `.${Keys}${ObjectKeyPaths<T[Keys],O>}`
    //: `.${Keys}`
    : `.${Keys}`
    | `.${Keys}${ObjectKeyPaths<T[Keys],O>}`
    | `.[${Keys}]`
    | `.[${Keys}]${ObjectKeyPaths<T[Keys],O>}`
    | `[${Keys}]`
    | `[${Keys}]${ObjectKeyPaths<T[Keys],O>}`
    : never ;

Solution by justBadProgrammer #16033

interface SomeObject {
  [key: string]: unknown;
}

type SomeArray = unknown[];

type PathString<Path extends string | number> = Path extends number
  ? Path | `[${Path}]`
  : Path;

type SegmentPath<
  Path extends string | number,
  BasePath extends string = ""
> = BasePath extends ""
  ? PathString<Path>
  : Path extends number
  ? `${BasePath}.${PathString<Path>}` | `${BasePath}${PathString<Path>}`
  : `${BasePath}.${PathString<Path>}`;

type ArrayKeyPaths<
  Model extends unknown[],
  BasePath extends string = ""
> = Model extends []
  ? never
  :
      | SegmentPath<number, BasePath>
      | (Model[number] extends SomeObject
          ? ObjectKeyPaths<Model[number], SegmentPath<number, BasePath>>
          : never)
      | (Model[number] extends SomeArray
          ? ArrayKeyPaths<Model[number], SegmentPath<number, BasePath>>
          : never);

type ObjectKeyPaths<
  Model extends SomeObject,
  BasePath extends string = "",
  Keys extends keyof Model = keyof Model
> = Keys extends string | number
  ?
      | SegmentPath<Keys, BasePath>
      | (Model[Keys] extends SomeObject
          ? ObjectKeyPaths<Model[Keys], SegmentPath<Keys, BasePath>>
          : never)
      | (Model[Keys] extends SomeArray
          ? ArrayKeyPaths<Model[Keys], SegmentPath<Keys, BasePath>>
          : never)
  : never;

Solution by schemelev #15114

type ObjectKeyPaths<T extends object, P extends string = never> =
  P | {
    [K in keyof T & (string | number)]:
    T[K] extends object ? (
      ObjectKeyPaths<T[K], AddPrefix<P, K>>
     ) : AddPrefix<P, K>
  }[keyof T & (string | number)]

type AddPrefix<P extends string, Path extends  & string | number> =
  [P] extends [never] ? (
    `${Path}`
  ) : Path extends number ? (
    `${P}.${Path}` | `${P}[${Path}]` | `${P}.[${Path}]`
  ) : `${P}.${Path}`

Playground

Solution by teamchong #11753

type IsNumber<T> = T extends number ? `[${T}]` | `.[${T}]` : never

type ObjectKeyPaths<
  T extends object,
  Flag extends boolean = false,
  K extends keyof T = keyof T
> = K extends string | number
? (Flag extends true ? `.${K}` | IsNumber<K>: `${K}`) | 
  (
    T[K] extends Record<string, any>
      ? `${Flag extends true ? `.${K}` | IsNumber<K> : `${K}`}${ObjectKeyPaths<T[K], true>}`
      : never
  )
: never;

Solution by ProsperBao #10903

type ObjectKeyPaths<
  T extends object,
  Res extends string = "",
  K extends keyof T = keyof T
> = K extends K
  ? T[K] extends Record<keyof any, any>
    ?
        | (Res extends ""
            ? K
            : K extends number
            ? `${Res}.${K}` | `${Res}.[${K}]` | `${Res}[${K}]`
            : `${Res}.${K & string}`)
        | ObjectKeyPaths<
            T[K],
            Res extends ""
              ? `${K & (string | number)}`
              : K extends number
              ? `${Res}.${K}` | `${Res}.[${K}]` | `${Res}[${K}]`
              : `${Res}.${K & string}`
          >
    : Res extends ""
    ? K
    : K extends number
    ? `${Res}.${K}` | `${Res}.[${K}]` | `${Res}[${K}]`
    : `${Res}.${K & string}`
  : never;

Solution by DanielLin0516 #10803

type ObjectKeyPaths<T extends object, PreStr extends string = ''> = {
  [key in keyof T]: 
    ([PreStr, ''] extends ['', PreStr] ? key : `${PreStr}.${key & string}`) extends infer Key 
      ? T[key] extends ReadonlyArray<unknown> ? ArrayKeyPaths<T[key], Key & string> | Key
        : T[key] extends Record<PropertyKey, unknown> 
          ? ObjectKeyPaths<T[key], Key & string> | Key 
          : Key 
      : never 
}[keyof T];

type ArrayKeyPaths<T extends ReadonlyArray<unknown>, PreStr extends string = ''> = {
  [key in keyof T]: key extends string 
    ?([PreStr, ''] extends ['', PreStr] ? key : `${PreStr}.${key}` | `${PreStr}[${key}]` | `${PreStr}.[${key}]`) extends infer Key 
      ? T[key] extends ReadonlyArray<unknown> ? ArrayKeyPaths<T[key], Key & string> | Key
        : T[key] extends Record<PropertyKey, unknown> 
          ? ObjectKeyPaths<T[key], Key & string> | Key 
          : Key 
      : never 
    : never
}[number];

Solution by heretic-G #8612

type Paths<T, LAST extends string='', R extends keyof T = keyof T> 
= T extends Record<string|number, any> 
    ? `${LAST}.${R & string}` | (R extends any ? Paths<T[R], `${LAST}.${R & string}`> : never)
    : never
type ObjectKeyPaths<T> = `$${Paths<T>}`

Solution by lishion #8503

type ObjectKeyPaths<T extends object, Res extends string = '', K extends keyof T = keyof T>
  =
  (K extends K ?
    T[K] extends Record<keyof any, any> ?
    (Res extends '' ? K : K extends number ? `${Res}.${K}` | `${Res}.[${K}]` | `${Res}[${K}]`
      : `${Res}.${K & (string)}`) | ObjectKeyPaths<T[K], Res extends '' ? `${K & (string | number)}` : K extends number ? `${Res}.${K}` | `${Res}.[${K}]` | `${Res}[${K}]` : `${Res}.${K & (string)}`> : Res extends '' ? K : K extends number ? `${Res}.${K}` | `${Res}.[${K}]` | `${Res}[${K}]` : `${Res}.${K & (string)}`
    : never)


Solution by EGyoung #8232

// your answers
type ObjectKeyPaths<T, P extends string = "",  K extends string | number = Extract<keyof T, T extends any[]? number: string>> =
  object extends T ? string :
  T extends  any[] ?  `${P}${K}` | `${P}[${K}]` | `[${K}]` | `${P}${SubKeys<T, K>}` :
  T extends object ? `${P}${K}`| `${P}${SubKeys<T, K>}` :
  never;

Solution by venusliang #8136

type GenNode<K extends string | number,IsRoot extends boolean> = IsRoot extends true? `${K}`: `.${K}` | (K extends number? `[${K}]` | `.[${K}]`:never)

type ObjectKeyPaths<
  T extends object,
  IsRoot extends boolean = true,
  K extends keyof T = keyof T
> = 
K extends string | number ?
  GenNode<K,IsRoot> | (T[K] extends object? `${GenNode<K,IsRoot>}${ObjectKeyPaths<T[K],false>}`:never)
  :never;

Solution by jiangshanmeta #7939

// your answers

type ObjectKeyPaths<T extends object> = T extends Record<string, any>
? {
    [P in keyof T]: T[P] extends Record<string, any>
      ? P extends string 
        ? `${P}.${ObjectKeyPaths<T[P]>}` | P 
        : never 
      : P
  }[keyof T]
: never;

Solution by ch3cknull #7745