1

I have services

abstract class Service {
  public abstract get(id?: string): null;
}

class BookService extends Service {
  public get = (id: string) => null
}

class ShopService extends Service {
  book = new BookService();
  public get = (id: string) => null
}

Now I want to get all classes that implement method "get".

class Services  {
   shop = new ShopService();
}

type PickProperties<T, P> = Pick<T, { [K in keyof T]: T[K] extends P ? K : never }[keyof T]>;

type Repositories = PickProperties<Services, typeof Service.prototype>;


So the Repositories contain only shop.

But I want to get nested classes that implement a "get" method as well.

I try to do it recursively but get an error


export type DeepPickProperties<T, P> = Pick<
  T,
  {
    [K in keyof T]: T[K] extends P ? K : T[K] extends Constructor ? PickProperties<T[K], P>[keyof T[K]] : never;
  }[keyof T]
>;

I expect that type of Repositories will be shop and book.

Playground

Egor Pashko
  • 354
  • 2
  • 8
  • "I expect that type of Repositories will be shop and book." <-- what exactly does this mean? Can you write out explicitly what you expect this to be? What keys and values will this object type have? – jcalz Sep 29 '22 at 13:50
  • 1
    Like, *maybe* [this](https://tsplay.dev/NakdEW) is what you're looking for, but it seems strange to want this, since those keys don't tell you where in the hierarchy the property you care about is. What if there are multiple properties with the same name somewhere in the hierachy; you'd get an intersection of value types. I'm having a hard time understanding the use case. – jcalz Sep 29 '22 at 14:01
  • @jcalz That is the correct solution. Thanks – Egor Pashko Sep 30 '22 at 06:32
  • @jcalz use cases - I use react-query where the key should be part of service, like ["book", id] or ["another Service that contains method get", id] I need to type the first argument. The goal is to predict to pass random strings as a key because if we have different keys it means we have a different cache. – Egor Pashko Sep 30 '22 at 06:41
  • Okay I will write up an answer explaining when I get a chance. – jcalz Sep 30 '22 at 12:55

1 Answers1

1

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

jcalz
  • 264,269
  • 27
  • 359
  • 360