Reputation: 494
Wondering if there's a way to use keyof
and string templates in typescript to make a union type based on both keys + subkeys
const object = {
level1Key1: {
some: "data"
},
level1Key2: {
nestedKey: { more: "data" },
anotherNestedKey: { more: "data" },
}
}
Is there any way to generate a type using typeof object
to make the following union type without using any hardcoded strings (as in not doing keyof typeof object.level1key2
)
type desiredType = "level1Key1" | "level1Key2.nestedKey" | "level1Key2.anotherNestedKey";
Upvotes: 2
Views: 1326
Reputation: 327754
Here's one possible approach:
type MyPaths<T extends object> = keyof T extends infer K ? K extends string & keyof T ?
T[K] extends object ? `${K}${PrependDot<MyPaths<T[K]>>}` : never
: never : never;
type PrependDot<T extends string> = [T] extends [never] ? "" : `.${T}`;
This defines a recursive conditional type called MyPaths<T>
which takes an object type T
and produces a union of dotted paths to property values which are something I'll call shallow objects.
For example, in {a: {b: {c: "d"} } }
, the only path you'd get is "a.b"
. The path "a"
points to {b:{c:"d"}}
, which is an object, but it has subproperties, so it isn't shallow. The path "a.b.c"
points to "d"
which isn't an object.
Only "a.b"
points to a shallow object, {c:"d"}
.
Here's how it works. First, we want MyPaths<T>
to take the set of keys keyof T
and split them up into individual string
keys K
. For each such key, we will perform the necessary type operation, and then we will collect the results into one big union. We do this by making MyPaths<T>
a distributive conditional type over keyof T
. Hence the keyof T extends infer K ? K extends string & keyof T ? ... : never : never
, where ...
is the type operation for each K
.
That type operation is another conditional type check; if the property type of T
at key K
(aka T[K]
) is not an object (aka object
), then we want to return nothing (aka never
) for this property. If the property is an object, then we need to prepend the current key K
to the result of a recursive call to MyPaths<T[K]>
, give or take a dot.
I mean, if MyPaths<T[K]>
is never
, then T[K]
is a shallow object and we want to just return the current key K
with no dot after it. But if MyPaths<T[K]>
is some union of strings, then we want to add a dot in between. That's pretty much the way PrependDot<T>
is implemented (put a dot before the string T
unless T
is never
, in which case, just use an empty string).
That's what `${K}${PrependDot<MyPaths<T[K]>>}`
, does. It's a template literal type that does the string concatenation of K
and MyPaths<T[K]>
, with PrependDot
taking care of whether or not to insert a dot after K
or just an empty string.
Let's test it out:
type DesiredType = MyPaths<typeof object>;
// type DesiredType = "level1Key1" | "level1Key2.nestedKey" | "level1Key2.anotherNestedKey"
Looks good to me!
Upvotes: 3