Skip to content
Advertisement

Is it possible to wrap a function so that the wrapper has the same arguments plus another argument which is situated after these arguments?

My goal here is to wrap an API function so that the wrapper has the same arguments as the API function and then also has one additional final parameter. The API function is very generic so the wrapper needs to take the types and parameters from this inside function.

My reasoning is that I need to enhance the API function with additional optional arguments. For another developer using this wrapper function, it would be an awful experience to have this optional argument as the first argument.

My current attempt is as follows:

const insideFunc = (a: string): string => {
  return a
}

const wrapperFunc = <F extends (...args: any[]) => any>(
  fn: F
): ((b?: string, ...args: Parameters<F>) => [ReturnType<F>, string]) => {
  return (b?: string, ...args: Parameters<F>):[ReturnType<F>, string] => {
    return [fn(...args), b]
  }
}

This is almost what I need however the issue is that the parameter b has to be before the arguments of the inside function.

In a parallel universe, the solution would have the rest arguments before the new parameter as follows:

const insideFunc = (a: string): string => {
  return a
}

const wrapperFunc = <F extends (...args: any[]) => any>(
  fn: F
): ((...args: Parameters<F>, b?: string) => [ReturnType<F>, string]) => {
  return (...args: Parameters<F>, b?: string):[ReturnType<F>, string] => { //Observe the difference in argument order.
    return [fn(...args), b]
  }
}

However, this errors due to the rest arguments having to be the final argument.

Is there another way of solving this so that the inside function arguments can be first in the list?

Answer

In argument lists of functions the spread must come after other arguments. However, the same is not true for tuple types.

That means you could declare args like:

(...args: [...args: Parameters<F>, b: string])

Note that each member of this tuple is named, which helps preserve intellisense hints about the names of the original arguments.

It does mean you have to parse the args yourself though, but that’s not hard:

const originalArgs = args.slice(0, -1)
const b = args[args.length - 1]
return [fn(...originalArgs), b]

Which seems to work when used like:

const insideFunc = (name: string, age: number, likes: string[]): string => {
  return `${name} is ${age} and likes ${likes.join(', ')}`
}

const fn = wrapperFunc(insideFunc)

console.log(fn(
    'Alex',
    39,
    ['cool stuff', 'awesome things'],
    'and also snowboarding'
))
//-> ["Alex is 39 and likes cool stuff, awesome things", "and also snowboarding"] 

And when you hover over the fn here you can see the argument names are preserved in the reported type:

const fn: (
  name: string,
  age: number,
  likes: string[],
  b: string
) => [string, string]

Working playground example


One issue with this solution is that if b is an optional argument and not provided.

Well, you could ask the inner function for its length, which returns the number of arguments that it accepts.

const originalArgs = args.slice(0, fn.length)
const b = args[fn.length + 1]

Playground

However if the inner function has optional arguments, or takes a spread like ...args that’s obviously going to complicate things. In fact, I think that would make it impossible to know what arguments are for you inner function and which are supposed to come after.


May I suggest an alternative API? Something like:

fn([original, args, here], extraArg)

That way it’s trivial to tell what goes with the function, and what is extra. I think that no amount of clever tuple types or array slicing will give you a perfect, works in every case, solution without clearly separating the original args from the extra args.

Or maybe as nested functions which only call the inner function when the extra arg is provided?

fn(original, args, here)(extraArg)
Advertisement