Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Typescript string dot notation of nested generic type that belongs to property of certain value

Tags:

typescript

This question is an extension of the one found here.

I have an object:

type exampleType = {
    propertyOne: string
    propertyTwo: number,
    propertyThree: {
        propertyFour: string,
        propertyFive: Date,
        propertySix: boolean,
    }
}

I'm looking for a type that would validate a dot-notation like string to a path of either string or Date. In the example above, this would mean it the type compiles to:

propertyOne | propertyThree.propertyFour | propertyThree.PropertyFive

Using the question previously asked above, the following is possible:

type PathsToStringProps<T> = T extends string ? [] : {
    [K in Extract<keyof T, string>]: [K, ...PathsToStringProps<T[K]>]
}[Extract<keyof T, string>];

type Join<T extends string[], D extends string> =
    T extends [] ? never :
    T extends [infer F] ? F :
    T extends [infer F, ...infer R] ?
    F extends string ? 
    `${F}${D}${Join<Extract<R, string[]>, D>}` : never : string;  

type Path = Join<PathsToStringProps<exampleType>, ".">

I'm trying to make the above solution generic, so that I could give Path two generic arguments: T, which would represent exampleType here, and V, which would be string|Date in my example above.

When I tried making exampleType generic:

type Path<T> = Join<PathsToStringProps<T>, ".">

I got this error: Excessive stack depth comparing types 'PathsToStringProps<T>' and 'string[]'.ts(2321)

Which I was able to solve by specifying that T must represent a Key-Value object:

type Path<T extends {[key: string]: any}> = Join<PathsToStringProps<T>, ".">

Moving on to restricting the type of value to path points to:

type PathsToStringProps<T, V> = T extends (V) ? [] : {
    [K in Extract<keyof T, string>]: [K, ...PathsToStringProps<T[K], V>]
}[Extract<keyof T, string>];

type Join<T extends string[], D extends string> =
    T extends [] ? never :
    T extends [infer F] ? F :
    T extends [infer F, ...infer R] ?
    F extends string ? 
    `${F}${D}${Join<Extract<R, string[]>, D>}` : never : string;  

type Path<T extends {[key: string]: any}, V> = Join<PathsToStringProps<T, V>, ".">

But I get an error:

error

Which disappears if I remove the generic argument V from Path, but keep it in PathsToStringProps:

type Path<T extends {[key: string]: any}> = Join<PathsToStringProps<T, string|Date>, ".">

Here's a TypeScript Playground of my final attempt at getting this to work.

like image 558
Ali Bdeir Avatar asked Oct 24 '25 14:10

Ali Bdeir


1 Answers

Your approach, where PathsToProps<T, V> generates paths as tuples, and then where Join<T, D> concatenates the tuple elements to form dotted paths, is problematic for the compiler, since both PathToProps<T, V> and Join<T, D> are recursive conditional types, which don't always compose very nicely, and often run afoul of circularity guards or performance problems. Maybe you could tweak things so as to get that working, but it wouldn't be my first choice.

Instead, since it doesn't seem like you actually care about the tuples at all, you could just concatenate the strings directly inside PathsToProps<T, V>. In other words, instead of building up the paths first and then concatenating them later, you concatenate throughout the process.

It could look like this:

type PathsToProps<T, V> = T extends V ? "" : {
    [K in Extract<keyof T, string>]: Dot<K, PathsToProps<T[K], V>>
}[Extract<keyof T, string>];

type Dot<T extends string, U extends string> = 
  "" extends U ? T : `${T}.${U}`

The implementation of PathsToProps is very similar to yours, except that instead of explicitly dealing with empty tuples [] and prepending to into tuples via [K, ...PathsToProps<T[K], V>>], we explicitly use empty strings "" and concatenate them via Dot<K, PathsToProps<T[K], V>.

The Dot<T, U> type is just a shorthand for connecting a string T with an already-dotted-string U. You put a dot between them unless U is empty (this is technically wrong if you have any objects with an empty string as a key. You don't, do you? I hope not). The point is to make sure that you don't end up with a trailing dot that you need to remove (if you always joined with a dot, then you would get paths like "foo.bar.baz.").

Let's test it out:

type ExampleType = {
    propertyOne: string
    propertyTwo: number,
    propertyThree: {
        propertyFour: string,
        propertyFive: Date,
        propertySix: boolean,
    }
}

type StrOrDateEx = PathsToProps<ExampleType, string | Date>
// type StrOrDateEx = "propertyOne" | "propertyThree.propertyFour" | 
//  "propertyThree.propertyFive"

Looks good! And no recursion warnings.

Playground link to code

like image 83
jcalz Avatar answered Oct 27 '25 03:10

jcalz



Donate For Us

If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!