0

Using TypeScript 4.5 and the following code, I need to find the paths of the properties of the Detail interface.

export interface Base {
  [key: string]: boolean | Base;
}

interface Detail extends Base {
  propertyA: boolean,
  propertyB: boolean,
  moduleC: {
    propertyD: boolean,
    propertyE: boolean,
  }
}

I need to get a type with 'propertyA' | 'propertyB' | 'moduleC.propertyD' | 'moduleC.propertyE'.

Generally, there are solutions already existent, for instance here: Typescript: deep keyof of a nested object

But: given that type Path<T> = ... gives me the paths of all keys of type T and writing const bad: Path<Detail> = 'propertyXY' I want TypeScript to warn me about the wrong path.

Available solutions find the paths and allow for things like syntax completion. However, all the solutions fail with the [key: string]: boolean | Base part in warning about wrong paths and so far I was unable find out why or how to fix this. Example (based on the aforementioned link):

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

type Path<T, D extends number = 10> = [D] extends [never] ? never : T extends Base ?
  { [K in keyof T]-?: Join<K, Path<T[K], Prev[D]>> }[keyof T] : "";

type Prev = [never, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10,
  11, 12, 13, 14, 15, 16, 17, 18, 19, 20, ...0[]]

const bad: Path<Detail> = 'moduleC.propertyXY' // there should be a warning here because propertyXY doesn't exist but there isn't

What is an implementation for Path that allows TypeScript to find wrong (in Detail not existing) paths?

Andreas
  • 125
  • 1
  • 9

1 Answers1

0

I finally found an answer based on this solution: https://stackoverflow.com/a/68060169/4874075

type Path<T extends Base, C = T> = C extends Base
    ? { [P in keyof C]: Path<T, C[P]> }
    : (C & { getPath(): string })

function property<T extends Base, C = T>(): Path<T, C>{
    return new Proxy({} as any, {
        get(_, name: string) {
            return _property(name)
        }
    })
}

function _property<T extends Base, C = T>(path: string): Path<T, C> {
    return new Proxy({
        getPath() {
            return path
        },
    } as any, {
        get(target, name: string) {
            if (name === 'getPath') {
                return target[name]
            }
            return _property(`${path}.${name}`)
        }
    })
}

const path: Path<Detail, boolean> = property<Detail>().moduleC.propertyXY; // this will give an error

console.log(path.getPath()); // this prints path in string form

In this solution Path<Detail, boolean> is the type of a path that ends at a boolean property, a "complete" path. This way, this code ensures that only these complete paths can be given for instance to a function. Incomplete paths, paths that have more children have the type Path<Detail, Detail> or Path<Detail>.

In the project only property() and Path will be exported, which makes sure that users of this code can not provide an initial path by using _property(), because it's path parameter has no check whether it's a valid path.

Andreas
  • 125
  • 1
  • 9