To be clear, it seems you want the output type to look like
type Repositories = {
shop: ShopService;
book: BookService;
}
as if all subproperties at every depth of the object tree were copied up into the top level before picking properties from it. Almost like you'd first turn {a: {b: {c: string}, d: number}, e: boolean}
into {a: {b: {c: string}, d: number}, b: {c: string, d: number}, c: string, d: number, e: boolean}
before picking.
My approach here looks like the following ugly thing:
type DeepPickProperties<T, V> = (
T extends object ? PickProperties<T, V> & (
{ [K in keyof T]: (x: DeepPickProperties<T[K], V>) => void }[keyof T] extends
(x: infer U) => void ? U : never
) : unknown
) extends infer U ? { [K in keyof U]: U[K] } : never;
Essentially what I am doing is: for every object type T
, first we just perform PickProperties<T, V>
to grab any properties that are at the top level already. Then we walk through all the properties T[K]
and accumulate the results of performing DeepPickProperties<T[K], V>
on it by intersecting them all together.
So DeepPickProperties<Services, ...>
would grab {shop: ShopService}
, and then walk into Services["shop"]
which looks like {book: BookService; get: ...}
. Performing DeepPickProperties<>
on this object produces {book: BookService}
, and then we walk back up and intersect these together to produce {shop: ShopService} & {book: BookService}
.
That's the general sketch, but the details are a bit tricky.
First let's look at this part which I'll call IntersectionOfDeepPickedSubproperties
:
{ [K in keyof T]: (x: DeepPickProperties<T[K], V>) => void }[keyof T] extends
(x: infer U) => void ? U : never
This performs something very much like a union to intersection conversion. Performing {[K in keyof T]: F<T[K]>}[keyof T]
produces a union of F<T[K]>
for all K
in keyof T
. Here F<T[K]>
is a function type with DeepPick...T[K]
in the parameter position. And it is a feature of conditional type inference that inferring from a parameter position in a union of functions produces an intersection of those parameters.
Then we look at this, which I'll call PickedButLotsOfIntersections
:
T extends object ?
PickProperties<T, V> & (IntersectionOfDeepPickedSubproperties) : unknown
which just intersects the shallowly-picked properties with the intersections of the deeply-picked properties, and produces the unknown
type for non-objects, because intersecting with unknown
is a no-op, while intersecting with never
produces never
. We don't want a single non-object property somewhere in the hierarchy to erase all the deeply-picked properties. So we use unknown
.
And finally, the whole thing is:
PickedButLotsOfIntersections extends infer U ? { [K in keyof U]: U[K] } : never;
which turns a bunch of intersections into a single object type... all we are doing is copying the intersection into a new type parameter U
, and then mapping over it to produce one object type with all the properties.
So, let's test it. First let me add some more stuff to make sure the right thing happens:
class BookService extends Service {
public get = (id: string) => null
otherThing = 4 // <-- we don't want this
}
class ShopService extends Service {
book = new BookService();
thing = 3 // <-- we don't want this
public get = (id: string) => null
anotherThing = { subThing: new BookService() } // <-- we DO want this
}
And that becomes:
type Repositories = DeepPickProperties<Services, typeof Service.prototype>;
/* type Repositories = {
shop: ShopService;
book: BookService;
subThing: BookService;
} */
Looks good!
That answers the question as asked, but beware. This sort of deeply-nested type manipulation function often seems to have strange edge cases. If you pass types in which are already recursive, or excessively generic, or have repeated property names, or index signatures, I don't know what might happen. The best case is that the behavior will happen not to bother you; more likely, you'll find that you need to tweak the definition to work; and at worst, you'll run into performance or circularity problems that make this unworkable. So proceed with care!
Playground link to code