Extending a set of functions to accept extra arguments in Typescript

Viewed 452

The premise

I've got a generic API shape that looks like this:

type Macro = (...args: any[]) => Promise<any>;

type ApiObj = {
    [k: string]: Macro;
}

I also have type definitions for the specific API group, like so:

type MyApi = {
    getUsers(id: number): Promise<number>
    getPost(title: string): Promise<string>
}

I want to extend each of these functions in the API to allow for an extra optional argument that looks like this:

type MyExtraArg = {
    message: string;
}

In order to do this, I have created some utility types:

type AppendOptional<I, T extends any[]> = [...T, I?];

function isExtraArg(obj: any): obj is MyExtraArg {
    return obj?.message;
}

type ExtendedFunction<T> = T extends (...args: infer U) => infer R ? (...args: AppendOptional<MyExtraArg, U>) => R : never;

type ExtendedApi<T extends ApiObj> = {
    [K in keyof T]: ExtendedFunction<T[K]>
}

Then I implement my wrapping function like so:

function wrap<T extends Macro>(f: T) {
    return (...args: AppendOptional<MyExtraArg, Parameters<T>>) => {
        const lastArg = args.splice(-1)[0];
        
        if(isExtraArg(lastArg)) {
            console.log(lastArg.message);
        }

        return f(...args);
    }
}

In this way, doing this (sorta) works:

const wrappedF = wrap(myApi.getUsers);

// Here I get correct intellisense with the extra optional argument

My problems

  1. Intellisense loses the args names, and transforms them into args_0, args_1 etc.

    type p = Parameters<MyApi["getUsers"]> // ← this preserves the labels
    type p2 = AppendOptional<MyExtraArg, p> // ← this loses them
    

    Any way to make my mapped type preserve the labels?

  2. More importantly, when I want to wrap the whole API object, I get into troubles:

    function wrapApi<T extends ApiObj>(api: T) {
    return Object.keys(api)
        .reduce((acc, curr: keyof T) => {
            acc[curr] = wrap(api[curr]); // ← Typescript complains here, error below: *
            return acc;
        }, {} as ExtendedApi<T>);
    }
    
    const wrappedApi = wrapApi(myApi); // This works as above, i.e. correct intellisense, lost labels
    
    wrappedApi.getPost("a", { message: "Hello world" }); // This works, as above.
    

Typescript complains where the asterisk is at, error is

Type '(...args_0: Parameters<T[keyof T]>, args_1?: MyExtraArg | undefined) => Promise<any>' is not assignable to type 'ExtendedFunction<T[keyof T]>'.

Full Typescript playground link

1 Answers

The main reason why it won't work:

A generic type with conditional type which depends on type variable cannot be resolved until you pass a specific type. Constraint to type variable is not enough to resolve such generic type.

My considerations:

I think constraint to type variable should be sufficient for resolving these types of generics.

One of the solutions could be:

Typescript playground

Related