I need to turn nested generic types into a array of arrays based on how deeply nested it was.
Context
I'm building a query builder for linked data. The dev using this can use regular objects with get/set methods to select data in a graph.
Why I currently use intersection
When making a query with Person.select() the dev should have access to all the regular get/set methods of the Person class. But also some query specific things like filter(). This is why I wrapped everything in Query specific objects, which act as a proxy to the original object.
Hence Im converting the type Shape to QueryShape<..> & MappedShape<..> , where MappedShape takes all the properties of the original shape and converts their type to mapped query objects.
The problem
the infer keyword does not seem to work with intersection.
When inspecting result and result2, you will see string and string[]. But for result3 and result4 the type is unknown.
I'm open to any other setups.
The example below is a very simplified minimal setup to demonstrate my question.
The expected result of p.friend.friend is Person[][], please ignore that doesn't fully make sense, I avoided a lot of extra complexity with singular/plural. It it helps, think persons.
Playground setup
See a live example in the TS playground here
Code
simplified setup in the library:
type ToQueryValue<T,Source=null> = T extends string
? QueryString<Source>
: T extends Shape
? QueryShape<T,Source> & MappedShape<T,QueryShape<T,Source>>
: never;
type MappedShape<T extends Shape,Source> = {
[P in keyof T]: ToQueryValue<T[P],Source>;
};
type ToDataResult<T> =
T extends QueryShape<infer ShapeType,infer Source>
? ToDataValue<ShapeType,Source>
: T extends QueryString<infer Source>
? ToDataValue<string,Source>
: never;
type ToDataValue<Value,Source> = Source extends QueryShape<infer ShapeType, infer ParentSource>
? ToQueryShapeSource<ShapeType,Value, ParentSource>
: Value;
//if no source, then just return the value type, else, continue to go deeper back in the path once followed and add an array to the result type
export type ToQueryShapeSource<ShapeType, Value, ParentSource> =
ShapeType extends null ? Value : ToDataValue<Value, ParentSource>[];
class Shape {
static select<T extends Shape,QueryResult>(
this: {new (node: Node): T},
selectFn:(p:ToQueryValue<T,T>)=>QueryResult)
{
let p = new Shape();
let queryShape:QueryShape<T,T> = new QueryShape(p);
let traceResult = selectFn(queryShape as ToQueryValue<T,T>);
//omitted: path of used properties gets retraced and data gets loaded based on traceResult
//the actual shape of the data returned is determined by ToDataResult
return '' as ToDataResult<QueryResult>;
}
}
class QueryValue<ValueType,Source> {}
class QueryString<Source> extends QueryValue<string,Source>{ }
class QueryShape<ShapeType,Source=null> extends QueryValue<ShapeType,Source>{
constructor(public original:Shape) {
//creates a proxy for originalObject
super();
}
filter(filterFn?:any) {
//omitted
return this;
}
}
Example use of library:
//simplified class
class Person extends Shape {
get friend():Person { return new Person() }
get name() { return ''; }
}
//this works, type of result is string[]
let result = Person.select((p) => {
return p.name;
})
//this works as well! type of result is string[][] based on a deeper nested path!
let result2 = Person.select((p) => {
return p.friend.name;
})
//perfect, string[][][]
let result2b = Person.select((p) => {
return p.friend.friend.name;
})
//But these both do not work, the type of the result is unknown
let result3 = Person.select((p) => {
return p.friend;
})
let result4 = Person.select((p) => {
return p.friend.friend;
})