user3241778
user3241778

Reputation: 376

Extract method Names of Array of Classes into Type Union

I have an Array of Classes and want to extract all of their methods into one type Union.

For example:

class A{
  getBooks(): Book[]{}
  getBook(): Book{}
}

class B{
  getUsers(): User[]{}
  getUser(): User{}
  getBooksOfUser(userId: string)Book[]{}
}

I would like to have one type which has all Methods like this:

type allMethods = "getBooks" | "getBook" | "getUsers" | "getUser" | "getBooksOfUser";

I tried a few different Things but couldn't get it working:

const testResolvers = <const> [A, B]
type ExtractFunctionKeys<T> = { [P in keyof T]-?: T[P] extends Function ? P : never}[keyof T]
export type ValuesOf<T extends any[]>= ExtractFunctionKeys<T[number]>;

type Foo = ValuesOf<typeof testResolvers>;

Thanks for any help!

Upvotes: 2

Views: 285

Answers (1)

ghybs
ghybs

Reputation: 53290

There are 2 main issues in the above attempt:

  1. T[number] (indexed access type) produces an union of every type in the T tuple/array; TypeScript keeps only the intersection of keys (shared keys) as valid when using a variable that is a such union; in your case, A | B has no common key, so keyof (A | B) is already never (i.e. even without filtering for methods)
  2. typeof testResolvers is readonly [typeof A, typeof B], i.e. it distributes the typeof operator to the tuple elements, and we get the type of the class constructor instead of the actual class types; then when accessing their keys (with keyof typeof A for example), all we get is "prototype"

Issue 1: instead of a union, we want an intersection, for which TypeScript keeps the union of keys (all keys in any element of the intersection) as valid. There is no easy way to build such an intersection from a tuple (we cannot use the indexed access notation). But it is still do-able, e.g.:

// Convert a tuple into an intersection of every type in the tuple
type TupleToIntersection<T> =
    T extends readonly never[]
    ? unknown : (
        T extends readonly [infer First, ...infer Rest]
        ? First & TupleToIntersection<Rest>
        : never)

type intersection = TupleToIntersection<typeof testResolvers>
//   ^? type intersection = typeof A & typeof B

Issue 2: to get back the class type from its constructor type, we just need to make sure to access the "prototype" property, typically via indexed access again. We can even build a helper type to perform the conversion conditionally if necessary:

// Get the class (prototype) from its constructor, if available
type ConstructorToPrototype<C> = C extends { prototype: infer P } ? P : C

type AClass = ConstructorToPrototype<typeof A>
//   ^? type AClass = A

Then we can insert these helper types into the chain to get the desired behaviour of extracting the union of methods of all classes in the tuple:

// 1. Convert the tuple type into an intersection
// 2. Get the classes instead of constructor types
// 3. Get the keys which have function values (i.e. methods)
type ValuesOf<T extends readonly unknown[]> = ExtractFunctionKeys<ConstructorToPrototype<TupleToIntersection<T>>>;

type Foo = ValuesOf<typeof testResolvers>;
//   ^? type Foo = "getBooks" | "getBook" | "getUsers" | "getUser" | "getBooksOfUser"

Playground Link

Upvotes: 2

Related Questions