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
MethodNames, though?