Let's say I have a model:
type MyModel = {
string: string
subObject: {
subObjString: string
}
}
I would like to have a type so all those tests passes:
{ string: 'foo' }
{ subObject: { subObjString: 'foo'} }
{ 'subObject.subObjString': 'foo' }
{ 'subObject.notExistingprop': 'foo' }
{ notExisting: 'foo' }
My goal is to type a filter input for a mongoDb ORM where filters can be written with classic and dotted syntax.
My first attempt was to get the keys type right, so here is what I have done so far:
type FilterType<O extends Record<string, any>> = {
[K in (keyof O & string)]: O[K] extends Record<string, any> ? K extends symbol ? never : `${K}.${FilterType<O>}` : never
}[string]
type MyModel = {
string: string
subObject: {
subObjString: string
}
}
type FilterTypeForModel = FilterType<MyModel> // interpreted as unknown
const test1 = ['string', 'subObject'] satisfies FilterTypeForModel // should work
const test2 = ['subObject.subObjString'] satisfies FilterTypeForModel // should work
const test3 = ['subObject.notExistingProp'] satisfies FilterTypeForModel // Should not work
Here is how to get all possible paths from an object as a union type:
type DottedKeys<T> = {
[K in keyof T]:
K extends string
? T[K] extends object
? K | `${K}.${DottedKeys<T[K]>}`
: K
: 'none'
}[keyof T]
type MyModel = {
string: string;
subObject: {
subObjString: string;
subObj2: {
string3lv: string
number3lv: number
}
};
};
// ObjectAllPaths contains all possible paths
type ObjectAllPaths = DottedKeys<MyModel> // "string" | "subObject" | "subObject.subObjString" | "subObject.subObj2" | "subObject.subObj2.string3lv" | "subObject.subObj2.number3lv"
// Paths Test cases
type Test1 = 'string' extends ObjectAllPaths ? true : false; // Should be true
type Test2 = 'subObject' extends ObjectAllPaths ? true : false; // Should be true
type Test3 = 'subObject.subObjString' extends ObjectAllPaths ? true : false; // Should be true
type Test4 = 'subObject.notExistingProp' extends ObjectAllPaths ? true : false; // Should be false
type Test5 = 'notExisting' extends ObjectAllPaths ? true : false; // Should be false
To go further, as you said that you need that type to be an object finally, here is my attempt to provide an object version:
type OnlyDotted<T> = {
[K in keyof T]:
K extends string
? T[K] extends object
? `${K}.${DottedKeys<T[K]>}`
: never
: never
}[keyof T]
type AsFilter<T extends Record<string, any>> = Partial<{
[K in keyof T]: T[K] extends Record<string, any> ? AsFilter<T[K]> : T[K]
} & { [key in OnlyDotted<T>]: any }>
type FilterModel = AsFilter<MyModel>
const modelFilter: FilterModel = {
"subObject.subObjString": true, // ✅ dotted path ❌ failed type validation here, need to be improved
subObject: {
"subObj2.string3lv": true, // ✅ recursive dotted path ❌ failed type validation here, need to be improved
subObj2: {
string3lv: 'string', // ✅ normal path
number3lv: true // ✅ should error, type validation working
}
}
}
NOTE: this is not perfect since as you see in comments all dotted paths are typed as any. I couldn't find a better solution for now