Home > OS >  Typescript how to create a type getting all nested object key
Typescript how to create a type getting all nested object key

Time:02-21

In Typescript, I have a nested object variable:

const obj = {
    k1: {
        k1A: "k1A",
        k1B: "k2B",
        k1C: {
            k1C1: "k1C1",
            k1C2: "k1C2",
        },
    },
    k2: {
        k2A: {
            k2A1: "k2A1",
            k2A2: "k2A2",
        },
        k2B: {
            k2B1: "k2B1",
            k2B2: "k2B2",
        },
    },
    k3: {
        k3A: 'K3A'
    }
}

I have a function, which will lookup and ouput the deepest and nested object key path.

function getKeyPath(originalObj) {}

and the function run like:

const keyPath = getKeyPath(obj)

and the output will be:

{
    k1A: ["k1"],
    k1B: ["k2B"],
    K1C1: ["k1", "k1C"],
    k1C2: ["k1", "k1C"],
    k2A1: ["k2", "k2A"],
    k2A2: ["k2", "k2A"],
    k2B1: ["k2", "k2B"],
    k2B2: ["k2", "k2B"],
    k3A: ["k3"],
}

with Typescript, what I current set is:

function getKeyPath<T>(originalObj:T):{
    [x in string]: string[]
} {}

Is there a way, I can have a narrow type check? So instead of any string, I limit the return keys are from key of obj?

If I use type key = keyof typeof obj, I can only get the fist level of key, but not the deeper one.

CodePudding user response:

Before, you read my answer please double check your obj shape. It looks like there is some inconsistency. k1B key has k2B value. I think it should be k1B, but I might be mistaken. Also, k3A key has uppercased K3A. Is this by design?

In order to transform obj interface to expected output, we need to recursively iterate through obj interface and accumulate each key into array of elements. Once we reach key which has primitive value, in our case k1A: "k1A" we need to exit the loop and return current key and a tuple of all keys we have passed through.

Consider this example:


type Data = {
    k1: {
        k1A: "k1A",
        k1B: "k1B",
        k1C: {
            k1C1: "k1C1",
            k1C2: "k1C2",
        },
    },
    k2: {
        k2A: {
            k2A1: "k2A1",
            k2A2: "k2A2",
        },
        k2B: {
            k2B1: "k2B1",
            k2B2: "k2B2",
        },
    },
    k3: {
        k3A: 'k3A'
    }
}

// credits goes to https://stackoverflow.com/a/50375286
type UnionToIntersection<U> = (U extends any ? (k: U) => void : never) extends (
    k: infer I
) => void
    ? I
    : never;

type Values<T> = T[keyof T]

type Iterate<Obj, Path extends any[] = []> =
    UnionToIntersection<
        Obj extends string
        ? Record<Obj, Path>
        : Values<{
            [Prop in keyof Obj]:
            Prop extends Obj[Prop]
            ? Iterate<Obj[Prop], Path>
            : Iterate<Obj[Prop], [...Path, Prop]>
        }>>

type MakeReadable<T> = {
    [Prop in keyof T]: T[Prop]
}

// type Result = {
//     k3A: ["k3"];
//     k1A: ["k1"];
//     k1B: ["k1"];
//     k1C1: ["k1", "k1C"];
//     k1C2: ["k1", "k1C"];
//     k2A1: ["k2", "k2A"];
//     k2A2: ["k2", "k2A"];
//     k2B1: ["k2", "k2B"];
//     k2B2: ["k2", "k2B"];
// }
type Result = MakeReadable<Iterate<Data>>

Playground

Please let me know if it work and my assumptions are correct. If yes, I will provide you with more explanation.

CodePudding user response:

@captain-yossarian's answer is effective; I just wanted to throw in my two cents with mapped types:

type DeepestKeys<T> = T extends string ? never : {
    [K in keyof T & string]: T[K] extends string ? K : DeepestKeys<T[K]>;
}[keyof T & string];

type DeepestPaths<T, Path extends string[] = []> = T extends string ? Path : {
    [K in keyof T & string]: DeepestPaths<T[K], [...Path, K]>;
}[keyof T & string];

type ExcludePath<T, Key extends string, Path extends string[] = []> = T extends string ? Path : {
    [K in keyof T & string]: K extends Key ? T[K] extends string ? never : ExcludePath<T[K], Key, [...Path, K]> : ExcludePath<T[K], Key, [...Path, K]>;
}[keyof T & string];

type PathTo<T, Key extends string> = Exclude<DeepestPaths<T>, ExcludePath<T, Key>> extends [...infer Path, infer _] ? Path : never;

type GetKeyPaths<T> = { [K in DeepestKeys<T>]: PathTo<T, K>; };

I also think it's a bit more readable and easy to understand; so let me walk you through how it works.

First we make a type that gets the deepest nested keys. We go through each key in T. If the value is a string, then we've probably found a deepest key. Otherwise we "call" DeepestKeys on the value. Note that DeepestKeys ALWAYS gives us strings. The base case T extends string ? never : is to satisfy the compiler that DeepestKeys is not infinite. So after we go through each key, we access each key using [keyof T & string], which gives us a union of the deepest keys that were found.

Next is DeepestPaths. We find the deepest paths leading to the deepest keys in a similar fashion. This one is more simple because it's just recursion and keeping track of the path it took. When it finds a string it gives us the path it found. Then in the same way, [keyof T & string] gives us a union of the path.

Then we've got ExcludePaths, which does the same thing as DeepestPaths but you give it a key to exclude from the resulting union of paths. You do this with some conditional types and never. Because A | never is the same thing as A it's effectively excluding that path.

So that means if you exclude ExcludePaths from DeepestPaths you actually get the desired path. This is what PathTo does for us. It also removes the last key because we don't need it.

Finally, GetKeyPaths goes through each deepest key and gets the path to it.

Playground

  • Related