Andres Riofrio
Andres Riofrio

Reputation: 10367

How to type a function that wraps another function by name in TypeScript

Here is what I have so far:

function wrapCallByName<T extends any[], R>(functionName: keyof some.api.Api) {
  return (...args: T) => {
    try {
      some.api()[functionName](...args);
    } catch (error) {
      myHandleApiError(error);
    }
  }
}

It says that some.api()[functionName] could be undefined and that it doesn't know what types its arguments would be.

But it couldn't (i.e. probably won't) be undefined because of the type of functionName, and we do know what the types will be.

Ideally, the return type of wrapCallByName is the function signature of some.api()[functionName].

Is there a way to type this correctly in TypeScript?

Upvotes: 1

Views: 152

Answers (1)

Titian Cernicova-Dragomir
Titian Cernicova-Dragomir

Reputation: 250196

There are two parts to this problem, the first is getting the public signature of the function right. We want the function to take in a key to some.Api and return a value of the same type as the original key. To do this we will need an extra type parameter K that extends keyof some.Api and we will use type query to tell the compiler the return is the same as the type of the passed in field (some.Api[K])

declare namespace some {
    type Api = {
        foo(n: number): string;
        bar(s: string, n: number) : string
    }
    function api(): Api;
}


function wrapCallByName<K extends keyof some.Api>(functionName: K) : some.Api[K] {
    return ((...args: any[]) => {
        try {
            return (some.api()[functionName] as any)(...args);
        } catch (error) {
            throw error;
        }
    }) as any;
}

const foo = wrapCallByName('foo')
foo(1) //returns string

const bar = wrapCallByName('bar')
bar('', 1) //returns string

Playground

As you can see the above implementation has a lot of type assertions to any. This is because there are several problems. Firstly the index access to the api will result in an uncallable union of all fields in the API. secondly the function we return is not compatible with any field of the api. To get around this we can use an extra indirection that will make the compiler look at the values of the object as just functions with the signature (... a: any[]) =>any. This will eliminate the need for any assertions.

declare namespace some {
    type Api = {
        foo(n: number): string;
        bar(s: string, n: number): string
    }
    function api(): Api;
}

function wrapBuilder<T extends Record<keyof T, (...a: any[]) => any>>(fn: () => T) {
    return function <K extends keyof T>(functionName: K): T[K] {
        return ((...args: any[]) => {
            try {
                return fn()[functionName](...args);
            } catch (error) {
                throw error;
            }
        });
    }
}
const wrapCallByName = wrapBuilder(() => some.api());
const foo = wrapCallByName('foo');
foo(1); //returns string

const bar = wrapCallByName('bar');
bar('', 1); //returns string

Playground

I mention this nice, no assertion implementation because your question specifically denands it, personally I would be comfortable enough with the first version, especially if you api exposes only functions. The wrapper function needs to be written only once and I can't think of a situation where the assertions will cause a problem, the more important part is the for the public signature to forward the types correctly.

Upvotes: 1

Related Questions