I am trying to define a type that can receive any type that is an object and it returns a type that only accepts dotted paths (prop1.prop2.prop3
) to string
or string[]
. So far this is what I have:
type MyType<ObjectType extends Record<string, unknown>> = {
[Key in keyof ObjectType & string]: ObjectType[Key] extends Record<
string,
unknown
>
? MyType<ObjectType[Key]> extends ""
? ""
: `${Key}.${MyType<ObjectType[Key]>}`
: ObjectType[Key] extends Array<Record<string, unknown>>
? MyType<Unpacked<ObjectType[Key]>> extends ""
? ""
: `${Key}.${MyType<Unpacked<ObjectType[Key]>>}`
: ObjectType[Key] extends Array<string> | string
? `${Key}`
: "";
}[keyof ObjectType & string];
The bad thing is that this type also returns the parent prop of a prop that is of type string
or string[]
. For example, if a have the following type:
type ExampleType = {
detail: {
prop1: string;
subDetail: {
field1: string;
field2: number;
field3: {
inner: string[];
};
field4: number[];
};
};
arrOfObjs: {
name: string;
age: number;
ages: number[];
}[];
};
Then MyType<ExampleType>
incorrectly accepts arrOfObjs.
and detail.subDetail.
, but it accepts, as expected: arrOfObjs.name
, detail.prop1
, detail.subDetail.field1
and detail.subDetail.field3.inner
.
Can anyone point out what I'm missing? Thank you in advance for your time.
CodePudding user response:
I could not fix your type definition since it contains the type Unpacked
that you did not specify in the question. But I could write a type that seems to achieve what you want.
type PathOfString<T, P extends string = ""> = {
[K in keyof T & string]: T[K] extends Record<string, unknown>
? PathOfString<T[K], `${P}${K}.`> extends infer S
? `${S & string}`
: never
: T[K] extends string | string[]
? `${P}${K}`
: T[K] extends Record<string, unknown>[]
? PathOfString<T[K][number], `${P}${K}.`> extends infer S
? `${S & string}`
: never
: never
}[keyof T & string]
The logic of this type is quite simple: Its a recursive type that traverses the type T
to find string
or string[]
leaves. We also have the type P
which stores the Path of each branch.
We start by mapping over the keys of T
and indexing the mapped type with [keyof T & string]
to create a union.
{
[K in keyof T & string]: /* ... */
}[keyof T & string]
For T[K]
there are three different cases we have to handle.
T[K]
is an object. Here we recurively callPathOfString
by passingT[K]
and appendingK
toP
with a dot at the end.
T[K] extends Record<string, unknown>
? PathOfString<T[K], `${P}${K}.`> extends infer S
? `${S & string}`
: never
: /* ... */
T[K]
is eitherstring
ofstring[]
. Here we can just appendK
toP
and return it.
T[K] extends string | string[]
? `${P}${K}`
: /* ... */
T[K]
is an array of objects. We basically do the same thing we did for the object case but we pass all properties of the object inside the array toPathOfString
withT[K][number]
.
T[K] extends Record<string, unknown>[]
? PathOfString<T[K][number], `${P}${K}.`> extends infer S
? `${S & string}`
: never
: never
You might have wondered why the extends infer S
is necessary. Without it, the type does not fully evaluate for some reason. If you remove it, you can see that the type is technically still correct but not really human readable. Hover over the type here to see what I mean.
Let's see if it works:
type T0 = PathOfString<ExampleType>
// type T0 = "detail.prop1" | "detail.subDetail.field1" | "detail.subDetail.field3.inner" | "arrOfObjs.name"
Looks right to me.