Search code examples
typescripttypescript-generics

Extend `...args: Parameters<T>` to allow addition of more optional paramters at the end


I'm building a generic function which expects a function and returns a new "extended" function.

The returned function should expect the same parameters as the given base function + 1 more optional parameter at the end.

I figured out the typings to "re-use" the parameters from the given function on the returned function but I'm stuck on adding the additional optional parameter at the end.

What I have so far (the logging use case is just exemplary):

const asyncLog = async (log: string) => { /* ... */ };

function addLogging<T extends (...args: Parameters<T>) => ReturnType<T>>(fn: T, log: string) {
  const fnWithLogging = async (args: Parameters<T>, doLog = true) => {
    if (doLog) await asyncLog(log);
    return fn(...args);
  };
  return fnWithLogging;
}

const baseFn = async (a: number, b: string) => { /* ... */ };
const baseFnWithLogging = addLogging(baseFn, 'some log');
await baseFnWithLogging([1, '2'], false);

The shown code basically works but I don't want to pass the "base parameters" as array but want to pass all as plain parameters like baseFnWithLogging(1, '2', false).

Changing fnWithLogging(args: Parameters<T> to fnWithLogging(...args: Parameters<T> would seem to basically do the trick but I can't add the additional parameter doLog then because I get TS error 1014 A rest parameter must be last in a parameter list. then.

How can I make this work? E.g. could I do something like extend<Parameters<T>, doLog = true>?


Solution

  • You can use the rest parameter syntax for the function then use function.length to slice the passed arguments to the functions' and the optional wrapper params.

    There is a major caveat though, which is that this won't work if the passed function has any optional parameters because function.length won't count them and there's no straightforward way of getting them (except parsing the function string).

    const asyncLog = async (log: string) => { /* ... */ };
    
    function addLogging<T extends (...args: Parameters<T>) => ReturnType<T>>(fn: T, log: string) {
      const fnWithLogging = async (...args: [...Parameters<T>, doLog?: boolean]) => {
        const funcArgs = args.slice(0, fn.length) as Parameters<T>;
        const [doLog = false] = args.slice(fn.length) as [doLog: boolean];
        
        if (doLog) await asyncLog(log);
        return fn(...funcArgs);
      };
      return fnWithLogging;
    }
    
    const baseFn = async (a: number, b: string) => { /* ... */ };
    const baseFnWithLogging = addLogging(baseFn, 'some log');
    baseFnWithLogging(1, '2', false);
    

    Playground