Typescript: Deep Keyof of a Nested Object, with Related Type

Typescript: deep keyof of a nested object

UPDATE for TS4.1 It is now possible to concatenate string literals at the type level, using template literal types as implemented in microsoft/TypeScript#40336. The below implementation can be tweaked to use this instead of something like Cons (which itself can be implemented using variadic tuple types as introduced in TypeScript 4.0):

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

Here Join concatenates two strings with a dot in the middle, unless the last string is empty. So Join<"a","b.c"> is "a.b.c" while Join<"a",""> is "a".

Then Paths and Leaves become:

type Paths<T, D extends number = 10> = [D] extends [never] ? never : T extends object ?
{ [K in keyof T]-?: K extends string | number ?
`${K}` | Join<K, Paths<T[K], Prev[D]>>
: never
}[keyof T] : ""

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

And the other types fall out of it:

type NestedObjectPaths = Paths<NestedObjectType>;
// type NestedObjectPaths = "a" | "b" | "nest" | "otherNest" | "nest.c" | "otherNest.c"
type NestedObjectLeaves = Leaves<NestedObjectType>
// type NestedObjectLeaves = "a" | "b" | "nest.c" | "otherNest.c"

and

type MyGenericType<T extends object> = {
keys: Array<Paths<T>>;
};

const test: MyGenericType<NestedObjectType> = {
keys: ["a", "nest.c"]
}

The rest of the answer is basically the same. Recursive conditional types (as implemented in microsoft/TypeScript#40002) will be supported in TS4.1 also, but recursion limits still apply so you'd have a problem with tree-like structures without a depth limiter like Prev.

PLEASE NOTE that this will make dotted paths out of non-dottable keys, like {foo: [{"bar-baz": 1}]} might produce foo.0.bar-baz. So be careful to avoid keys like that, or rewrite the above to exclude them.

ALSO PLEASE NOTE: these recursive types are inherently "tricky" and tend to make the compiler unhappy if modified slightly. If you're not lucky you will see errors like "type instantiation is excessively deep", and if you're very unlucky you will see the compiler eat up all your CPU and never complete type checking. I'm not sure what to say about this kind of problem in general... just that such things are sometimes more trouble than they're worth.

Playground link to code




PRE-TS4.1 ANSWER:

As mentioned, it is not currently possible to concatenate string literals at the type level. There have been suggestions which might allow this, such as a suggestion to allow augmenting keys during mapped types and a suggestion to validate string literals via regular expression, but for now this is not possible.

Instead of representing paths as dotted strings, you can represent them as tuples of string literals. So "a" becomes ["a"], and "nest.c" becomes ["nest", "c"]. At runtime it's easy enough to convert between these types via split() and join() methods.


So you might want something like Paths<T> that returns a union of all the paths for a given type T, or possibly Leaves<T> which is just those elements of Paths<T> which point to non-object types themselves. There is no built-in support for such a type; the ts-toolbelt library has this, but since I can't use that library in the Playground, I will roll my own here.

Be warned: Paths and Leaves are inherently recursive in a way that can be very taxing on the compiler. And recursive types of the sort needed for this are not officially supported in TypeScript either. What I will present below is recursive in this iffy/not-really-supported way, but I try to provide a way for you to specify a maximum recursion depth.

Here we go:

type Cons<H, T> = T extends readonly any[] ?
((h: H, ...t: T) => void) extends ((...r: infer R) => void) ? R : never
: never;

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[]]

type Paths<T, D extends number = 10> = [D] extends [never] ? never : T extends object ?
{ [K in keyof T]-?: [K] | (Paths<T[K], Prev[D]> extends infer P ?
P extends [] ? never : Cons<K, P> : never
) }[keyof T]
: [];

type Leaves<T, D extends number = 10> = [D] extends [never] ? never : T extends object ?
{ [K in keyof T]-?: Cons<K, Leaves<T[K], Prev[D]>> }[keyof T]
: [];

The intent of Cons<H, T> is to take any type H and a tuple-type T and produce a new tuple with H prepended onto T. So Cons<1, [2,3,4]> should be [1,2,3,4]. The implementation uses rest/spread tuples. We'll need this to build up paths.

The type Prev is a long tuple that you can use to get the previous number (up to a max value). So Prev[10] is 9, and Prev[1] is 0. We'll need this to limit the recursion as we proceed deeper into the object tree.

Finally, Paths<T, D> and Leaves<T, D> are implemented by walking down into each object type T and collecting keys, and Consing them onto the Paths and Leaves of the properties at those keys. The difference between them is that Paths also includes the subpaths in the union directly. By default, the depth parameter D is 10, and at each step down we reduce D by one until we try to go past 0, at which point we stop recursing.


Okay, let's test it:

type NestedObjectPaths = Paths<NestedObjectType>;
// type NestedObjectPaths = [] | ["a"] | ["b"] | ["c"] |
// ["nest"] | ["nest", "c"] | ["otherNest"] | ["otherNest", "c"]
type NestedObjectLeaves = Leaves<NestedObjectType>
// type NestedObjectLeaves = ["a"] | ["b"] | ["nest", "c"] | ["otherNest", "c"]

And to see the depth-limiting usefulness, imagine we have a tree type like this:

interface Tree {
left: Tree,
right: Tree,
data: string
}

Well, Leaves<Tree> is, uh, big:

type TreeLeaves = Leaves<Tree>; // sorry, compiler ⌛br>// type TreeLeaves = ["data"] | ["left", "data"] | ["right", "data"] | 
// ["left", "left", "data"] | ["left", "right", "data"] |
// ["right", "left", "data"] | ["right", "right", "data"] |
// ["left", "left", "left", "data"] | ... 2038 more ... | [...]

and it takes a long time for the compiler to generate it and your editor's performance will suddenly get very very poor. Let's limit it to something more manageable:

type TreeLeaves = Leaves<Tree, 3>;
// type TreeLeaves2 = ["data"] | ["left", "data"] | ["right", "data"] |
// ["left", "left", "data"] | ["left", "right", "data"] |
// ["right", "left", "data"] | ["right", "right", "data"]

That forces the compiler to stop looking at a depth of 3, so all your paths are at most of length 3.


So, that works. It's quite likely that ts-toolbelt or some other implementation might take more care not to cause the compiler to have a heart attack. So I wouldn't necessarily say you should use this in your production code without significant testing.

But anyway here's your desired type, assuming you have and want Paths:

type MyGenericType<T extends object> = {
keys: Array<Paths<T>>;
};

const test: MyGenericType<NestedObjectType> = {
keys: [['a'], ['nest', 'c']]
}

Link to code

Typescript: deep keyof of a nested object, with related type

Below is the full implementation I have of Flatten<T, O> which transforms a type possibly-nested T into a "flattened" version whose keys are the dotted paths through the original T. The O type is an optional type where you can specify a (union of) object type(s) to leave as-is without flattening them. In your example, this is just Date, but you could have other types.

Warning: it's hideously ugly and probably fragile. There are edge cases all over the place. The pieces that make it up involve weird type manipulations that either don't always do what one might expect, or are impenetrable to all but the most seasoned TypeScript veterans, or both.

In light of that, there is no such thing as a "canonical" answer to this question, other than possibly "please don't do this". But I'm happy to present my version.

Here it is:



type Flatten<T, O = never> = Writable<Cleanup<T>, O> extends infer U ?
U extends O ? U : U extends object ?
ValueOf<{ [K in keyof U]-?: (x: PrefixKeys<Flatten<U[K], O>, K, O>) => void }>
| ((x: U) => void) extends (x: infer I) => void ?
{ [K in keyof I]: I[K] } : never : U : never;

The basic approach here is to take your T type, and return it as-is if it's not an object or if it extends O. Otherwise, we remove any readonly properties, and transform any arrays or tuples into a version without all the array methods (like push() and map()) and get U. We then flatten each property in that. We have a key K and a flattened property Flatten<U[K]>; we want to prepend the key K to the dotted paths in Flatten<U[K]>, and when we're done with all that we want to intersect these flattened objects (with the unflattened object too) all together to be one big object.

Note that convincing the compiler to produce an intersection involves conditional type inference in contravariant positions (see Transform union type to intersection type), which is where those (x: XXX) => void) and extends (x: infer I) => void pieces come in. It makes the compiler take all the different XXX values and intersect them to get I.

And while an intersection like {foo: string} & {bar: number} & {baz: boolean} is what we want conceptually, it's uglier than the equivalent {foo: string; bar: number; baz: boolean} so I do some more conditional type mapping with { [K in keyof I]: I[K] } instead of just I (see How can I see the full expanded contract of a Typescript type?).

This code generally distributes over unions, so optional properties may end up spawning unions (like {a?: {b: string}} could produce {"a.b": string; a?: {b: string}} | {"a": undefined, a?: {b: string}}, and while this might not be the representation you were going for, it should work (since, for example, "a.b" might not exist as a key if a is optional).


The Flatten definition depends on helper type functions that I will present here with various levels of description:

type Writable<T, O> = T extends O ? T : {
[P in keyof T as IfEquals<{ [Q in P]: T[P] }, { -readonly [Q in P]: T[P] }, P>]: T[P]
}

type IfEquals<X, Y, A = X, B = never> =
(<T>() => T extends X ? 1 : 2) extends
(<T>() => T extends Y ? 1 : 2) ? A : B;

The Writable<T, O> returns a version of T with the readonly properties removed (unless T extends O in which case we leave it alone). It comes from TypeScript conditional types - filter out readonly properties / pick only required properties.

Next:

type Cleanup<T> =
0 extends (1 & T) ? unknown :
T extends readonly any[] ?
(Exclude<keyof T, keyof any[]> extends never ?
{ [k: `${number}`]: T[number] } : Omit<T, keyof any[]>) : T;

The Cleanup<T> type turns the any type into the unknown type (since any really fouls up type manipulation), turns tuples into objects with just individual numericlike keys ("0" and "1", etc), and turns other arrays into just a single index signature.

Next:

type PrefixKeys<V, K extends PropertyKey, O> =
V extends O ? { [P in K]: V } : V extends object ?
{ [P in keyof V as
`${Extract<K, string | number>}.${Extract<P, string | number>}`]: V[P] } :
{ [P in K]: V };

PrefixKeys<V, K, O> prepends the key K to the path in V's property keys... unless V extends O or V is not an object. It uses template literal types to do so.

Finally:

type ValueOf<T> = T[keyof T]

turns a type T into a union of its properties. See Is there a `valueof` similar to `keyof` in TypeScript?.

Whew! /p>


So, there you go. You can verify how closely this conforms to your stated use cases. But it's very complicated and fragile and I wouldn't really recommend using it in any production code environment without a lot of testing.

Playground link to code

how do i get a deep Keyof with dot access paths that allow circular referencing

Your RelationsOf<T> is similar in spirit to Paths<T> as described in the answer to a similar question. Like Paths<T>, such deeply nested types are tricky and fragile, and can easily cause the compiler to complain about instantiation depth (if you're lucky) or get completely bogged down and unresponsive (if you're unlucky). For any recursive type, the number of potential paths through an object type tends to be exponential in the path depth. Depth limiting is therefore a good idea, although sometimes even with depth limiting there are problems with compiler performance.

Here's one way to implement RelationsOf<T, D> where D is an optional numeric literal type corresponding to the desired depth (if you don't specify D then it defaults to 3):

type RelationsOf<T extends object, D extends number = 3> =
[D] extends [0] ? never :
T extends object[] ? RelationsOf<T[number], D> :
keyof T extends infer K ?
K extends string & keyof T ?
T[K] extends object ?
K | `${K}.${RelationsOf<T[K], Prev[D]>}` :
never : never : never;

type Prev = [never, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15];

The Prev type is a helper so that the compiler can easily subtract one from anything you give as D (e.g., Prev[10] is 9); it currently stops at 15. You could make it longer, but you'll probably run into depth problems.

The way it works: to compute RelationsOf<T, D> we first check to see if D is 0. If so, we bail out. Otherwise we check to see if T is an array of objects. If so, we compute RelationsOf<T[number], D> where T[number] is the element type of the T array. (We do this to skip one level of depth corresponding to the keys of arrays; otherwise you'd end up with paths like `tags.${number}.companies` instead of "tags.companies".)

At this point we use a distributive conditional type to take keyof T and perform a type operation for each string union member K. So if keyof T is "name" | "country" | "tags", then K will be "name", "country", and "tags" in turn, and the result of our type operation will be joined in a union.

The type operation is: if the property type of T at key K (T[K]) is an object, then we compute K | `${K}.${RelationsOf<T[K], Prev[D]>}`. Meaning, we take the key itself plain, and we prepend it (and a dot) to the results of RelationsOf operating on the property type with the depth reduced by one. If T[K] isn't an object, then we use never, since we don't want that key in our output type.


Let's test it out:

type CompanyRelations = RelationsOf<Company>;
/* type CompanyRelations = "country" | "tags" | "tags.companies" |
"tags.countries" | "country.tags" | "country.companies" |
"country.tags.companies" | "country.tags.countries" |
"country.companies.country" | "country.companies.tags" |
"tags.companies.country" | "tags.companies.tags" |
"tags.countries.tags" | "tags.countries.companies" */

type CountryRelations = RelationsOf<Country>;
/* type CountryRelations = "companies" | "tags" | "tags.companies" |
"tags.countries" | "companies.tags" | "companies.country" |
"companies.tags.companies" | "companies.tags.countries" |
"companies.country.companies" | "companies.country.tags" |
"tags.companies.tags" | "tags.companies.country" |
"tags.countries.companies" | "tags.countries.tags" */

That looks right. Everything has a path length of at most 3. If we increase D, we see how the union gets longer (with the paths getting longer too):

type CompanyRelations4 = RelationsOf<Company, 4>
/* type CompanyRelations4 = "country" | "tags" | "tags.companies" |
"tags.countries" | "tags.companies.country" | "tags.companies.tags" |
"tags.countries.tags" | "tags.countries.companies" | "country.tags" |
"country.companies" | "country.companies.country" |
"country.companies.tags" | "country.tags.companies" |
"country.tags.countries" | "country.tags.companies.country" |
"country.tags.companies.tags" | "country.tags.countries.tags" |
"country.tags.countries.companies" | "country.companies.tags.companies" |
"country.companies.tags.countries" | "country.companies.country.tags" |
"country.companies.country.companies" | "tags.companies.tags.companies" |
"tags.companies.tags.countries" | "tags.companies.country.tags" |
"tags.companies.country.companies" | "tags.countries.companies.country" |
"tags.countries.companies.tags" | "tags.countries.tags.companies" |
"tags.countries.tags.countries" */

If we try 10 the compile won't even list them out individually because there are more than 2000 entries;

type CompanyRelations10 = RelationsOf<Company, 10>;
/* type CompanyRelations10 = "country" | "tags" | "tags.companies" |
"tags.countries" | "tags.companies.country" | "tags.companies.tags" |
"tags.countries.tags" | "tags.countries.companies" | "country.tags" |
... 2036 more ... |
"tags.countries.tags.countries.companies.country.companies.country.tags.countries"
*/

Okay, looks good.

Playground link to code

TypeScript keyof of nested object type

There may well be edge cases, but here's an implementation that works for your example:

type KeysUnder<T, K extends PropertyKey> =
T extends object ? {
[P in keyof T]-?: (P extends K ? keyof T[P] : never) | KeysUnder<T[P], K>
}[keyof T] : never;

type ConfigOnKeys = KeysUnder<Config, "on">;
// type ConfigOnKeys = "START" | "PAUSE" | "RESET" | "HEAT"

The idea is to use a recursive conditional type to drill down into object properties. To get KeysUnder<T, K>, for an object T, we look at each property key P. If P matches K (which will be "on" in your use case), then you want the keys of its the property value type T[P]. The recursive bit is that we also join all these keys to KeysUnder<T[P], K>.

Playground link to code

How i do to access key of nested object in my interface with type generics?

You can look at this post for the possible options - Typescript: deep keyof of a nested object

For your specific example I think you can try the following:

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

type Paths<T> = T extends object
? { [K in keyof T]-?: K extends string
? `${K}` | Join<K, Paths<T[K]>>
: never
}[keyof T]
: never

function useForm<T extends object>(): {
watch: (key: Paths<T>) => void
} {
return { watch: () => {} }
}

Typescript: Create union type based on existence of sub keys in an object

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!

Playground link to code

In Typescript, how can retrieve a deeply nested property type where I know the key, but not the path?

It's ultimately up to you to decide what you want to see in various edge case situations, like multiple properties with the same property name at different depths of the object, or what to do in the face of unions or optional properties or index signatures, etc. For now I will mostly ignore such issues, since it's nearly impossible to anticipate all possible uses and address them.


Here's how one might type GetDeepProp<T, K>:

type GetDeepProp<T extends object, K extends string> = K extends keyof T
? T[K] : { [P in keyof T]: GetDeepProp<Extract<T[P], object>, K> }[keyof T]

This is similar to yours except that GetDeepProp<Extract<T[P], object>, K> will distribute over unions in T[P], and in the case where T[P] is not an object, this evaluates to never and not unknown.

It's true that {[P in keyof T]: ...}[keyof T] is a mapped type into which you immediately index to get a union of all the property values. The reason this will work for you is that for irrelevant keys you want the mapped type's property values to be the never type, since the compiler collapses XXX | never to XXX for all types XXX: the never gets absorbed in the union.

So you don't want unknown because it has the opposite behavior with unions: the compiler collapses XXX | unknown to just unknown for all types XXX. You don't want sibling properties to obliterate your result with unknown:

type DeepUser = { account: { region: number; }, other: number }
let oops: OrigGetDeepProp<DeepUser, 'region'> // unknown for your definition
let deepRegion: GetDeepProp<DeepUser, 'region'> // number for new definition

The other property destroys the result with your version of GetDeepProp, while this version gives number.


There you go. As I said, you will probably have some use cases where this definition does things you don't expect or like. If they are minor, I might be able to update the answer to deal with them. Otherwise, you might need to address this yourself or make a new post with more detailed requirements.

But hopefully this at least explains the general approach here enough to be useful to you.

Playground link to code



Related Topics



Leave a reply



Submit