2

I have an object that has multiple methods, each with different parameters and I am trying to define a wrapper function that will accept a method from this object and return a function with the same signature.

Here is an example from the TypeScript playground.

interface myFunctions{
    one: () => number;
    two: () => number;
    echo: (str: string) => string;
}

const myFunctions: myFunctions = {
    one: () => 1,
    two: () => 2,
    echo: (str: string) => str
}

This is my current wrapper definition:

const wrapper = <T extends keyof myFunctions> (
    func: myFunctions[T]
) => (...args: Parameters<myFunctions[T]>) => func(...args)

And this is an example of how I would like to use the wrapper:

wrapper<'one'>(myFunctions.one)()
wrapper<'echo'>(myFunctions.echo)('hello')

Typescript does not accept my definition for the wrapper, as Parameters<myFunctions[T]> can be either an empty array or the parameter str, and the function echo expects a string.

I am not very proficient in Typescript and went over the Typescript Handbook and could not find an answer. Similar questions regarding wrappers in StackOverflow were also not what I'm looking for.

What is the correct way to define wrapper?

tome
  • 23
  • 3

3 Answers3

2

Every time you start typing something, ask yourself: "if I were a compiler, what would I expect?". If you analyze the problem of wrapping a function, you will notice you need to let the compiler know that:

  1. The func parameter satisfies the (...args: any[]) => any constraint (i.e. "any function").
  2. args rest parameter has to be inferred (which you solved with Parameters utility type).
  3. return type of the resulting function also has to be inferred (this one you missed). It can be easily achieved with ReturnType utility type.

What you did not have to tell the compiler, is what the key is - it is smart enough to infer the type of the function from usage. Now, how could you rewrite your type? Combine steps 1 to 3, and this is what you get:

const wrapper = <U extends (...args: any[]) => any>(func: U) => (...args: Parameters<U>) : ReturnType<U> => func(...args);

const one = wrapper(myFunctions.one);   //() => number
const two = wrapper(myFunctions.two);   //() => number
const echo = wrapper(myFunctions.echo); //(str: string) => string

The resulting generic is not restricted to your myFunctions type:

const rand = wrapper((a:string,b:number,c:boolean) => {}); //(a: string, b: number, c: boolean) => void

If you need to only accept functions from myFunctions type, just let the compiler know that U must match exactly one of the signatures from myFunctions members.

To achieve that, you need to guarantee that the types are "identical". This can be done with a dual conditional type. Its general form looks something like this:

A extends B ? B extends A ? C : never : never;

Let's apply the idea to the use case and create a helper type:

type IsOneOf<T,F> = { [ P in keyof T ] : F extends T[P] ? T[P] extends F ? T[P] : never : never; }[keyof T];

The above will resolve to never if there are no matching members of T and to matching members otherwise. Since nothing is compatible with never, we get the desired behavior:

type IsOneOf<T,F> = { [ P in keyof T ] : F extends T[P] ? T[P] extends F ? T[P] : never : never; }[keyof T];

const wrapper = <U extends (...args: any[]) => any>(func: U extends IsOneOf<myFunctions, U> ? U : never) => (...args: Parameters<U>) : ReturnType<U> => func(...args);

const one = wrapper(myFunctions.one);
const two = wrapper(myFunctions.two);
const echo = wrapper(myFunctions.echo);
const rand = wrapper((a:string,b:number,c:boolean) => {}); //not assignable to parameter of type 'never'.
const hm = wrapper(() => 'a'); //also error

Playground

  • 1
    The approach you suggest with asking "what do I need to tell the compiler" is great, and I will use it. I do have only one problem with your suggested solution. If I call `wrapper(() => 'a')` for example, it does allow that. Why is that & how can it be fixed? See line 20 [here](https://cutt.ly/akBSfLc) – tome Feb 15 '21 at 07:21
  • @tome - well, this is an oversight on my part, let's restrict further :) – Oleg Valter is with Ukraine Feb 15 '21 at 08:28
0

There's a number of ways to solve this problem, mostly it depends on what the wrapper function should actually do in the end.

If you simply need to wrap a function you can get away with a very simple generic:

function wrapper<T extends Function>(input: T) { ... }

// Manually annotating T is not necessary, it's inferred by TS
wrapper(myFunctions.echo)('hello');

If it's imperative that the function comes from myFunctions, you could use something like this ValueOf generic:

type ValueOf<T> = T[keyof T];
function wrapper<T extends ValueOf<myFunctions>>(input: T) { ... }

wrapper(myFunctions.echo)('hello');

Whether one of these or some third alternative makes more sense depends on the end goal you're trying to achieve.

Etheryte
  • 24,589
  • 11
  • 71
  • 116
  • Note that my problem is with the definition of the return function, not with the definition of the wrapper itself (the part that is in ... in your example). I tried your suggestion, and I still get the same result. See Typescript playground [here](https://cutt.ly/BkV2oO3). Any chance you can share an example in the playground? Thanks! – tome Feb 14 '21 at 21:14
0

Probably this would help

function wrap<T extends Function>(fn: any): T {
    // if (fn.constructor.name === 'AsyncFunction') {
    //     return ((...args: unknown[]) => fn(...args)) as unknown as T
    // }
    return ((...args: unknown[])  => fn(...args)) as unknown as T
}

const asyncFunc = async (n: number) => {
    return Promise.resolve(1);
}
const func = (i: number, j: string) => i + j;

const x = wrap<typeof asyncFunc>(asyncFunc);


const yy = wrap<typeof func>(func);
yy(1, "2")

TS Playground

Ankit Kumar
  • 1,680
  • 1
  • 22
  • 35