Seth
Seth

Reputation: 145

Typescript: Get Type of Method Parameters from Generic

Is it possible to get the type of parameters of methods from a generic?

For example if I have:

interface Keys {
  create: any;
  ...
}

type MethodNames<T> = { [P in keyof Keys]: keyof T; }

Then is it possible to get the type of parameters of those Methods?

type MethodParams<T> = { [P in keyof Keys]: Parameters<T[???]>; }

Playground Link to Relevant Code

Upvotes: 1

Views: 691

Answers (1)

jcalz
jcalz

Reputation: 328152

The only way I can imagine this working is if MethodParams is not only generic in T, the type of the underlying API, but also in M, the particular MethodNames<T> appropriate for T. And that could look like this:

type Keys = "create" | "getOne" | "getAll" | "update" | "delete"

export type MethodNames<T> = {
  [K in Keys]: keyof T;
};

export type MethodParams<T, M extends MethodNames<T>> = {
  [K in Keys]: T[M[K]] extends (...args: infer P) => any ? P : never;
};

(Note that I make Keys just a union of string literal types instead of making it an object type with values of type any that we ignore.)

So MethodParams<T, M> iterates over the keys in Keys and then looks up each key in M to get the key of T we want to probe for parameters. That is, if K is a member of Keys, we want to get the parameters from what we hope is a function type at T[M[K]]. We use conditional type inference to get that.


Let's test to see that it works. First I'll write a helper function to verify that a proposed method mapper works for a particular type T:

const methods = <T, M extends MethodNames<T>>(api: T, methods: M) => methods;

And now I'll make up a fake api:

interface SomeData {
  id: number,
  a: string,
  b: number,
  c: boolean
}
interface SomeApi {
  change(id: number, data: Partial<SomeData>): SomeData,
  destroy(id: number): boolean
  grab(id: number): SomeData | undefined,
  grabbingSpree(): SomeData[],
  make(data: Omit<SomeData, "id">): SomeData,
}
declare const someApi: SomeApi;

Here's the method mapper:

const someApiMethods = methods(someApi, {
  create: "make",
  getOne: "grab",
  getAll: "grabbingSpree",
  update: "change",
  delete: "destroy"
})

And now I can finally try MethodParams:

type SomeApiMethodParams = MethodParams<SomeApi, typeof someApiMethods>;
/* type SomeApiMethodParams = {
    create: [data: Omit<SomeData, "id">];
    getOne: [id: number];
    getAll: [];
    update: [id: number, data: Partial<SomeData>];
    delete: [id: number];
} */

Looks good. The type of SomeApiMethodParams is what we expect it to be.


Oh, and this also means that any class or type that needs to compute MethodParams will need to be generic in the appropriate MethodNames type. For example, your AbstractTestEnv class would be augmented with another type parameter:

export abstract class AbstractTestEnv<S, T, U, M extends MethodNames<S>> {
  public api: S;       
  public createDto: T;
  public crudMethods: M;    
  protected constructor(api: S, crudMethods: M) {
    this.api = api;
    this.crudMethods = crudMethods;
    this.createDto = this.generateCreateDto(this.resourceId);
  }   
  public abstract generateCreateDto(resourceId: string): T;
  public abstract getParams(): MethodParams<S, M>;
  /* snip */
}

Playground link to code

Upvotes: 1

Related Questions