Search code examples
typescriptgeneric-type-argument

How to wrap function and extend custom arguments with right type


I want wrap callback function and extend custom arguments, And i get error type alert

const func = (a: string) => {
   console.log(a);
}

type Options = {
  force?: boolean
};

const wrapFunc = <A extends any[], R>(fn: (...args: A) => R) => {
  const wrappedFunc = (...args: A & [options?: Options]) => {
     const options: Options = 'force' in args[args.length - 1] ? args.pop() : {};
     fn(...(args as A))
  }

  return wrappedFunc;
}

const newFunc = wrapFunc(func);

newFunc('a', {force: true})
// Argument of type '["a", { force: true; }]' is not assignable to parameter of type '[a: string] & // [options?: Options | undefined]'.
//  Type '["a", { force: true; }]' is not assignable to type '[a: string]'.
//    Source has 2 element(s) but target allows only 1.(2345)

here is reproduced TypeScript playground

Could somebody help me make is right. Thanks!


Solution

  • The & operator is not what you should use. number[] & {prop: string} will expect an array with a prop property typed as a string, not another element. To push an element to a generic array parameter, you can do as follows: <A extends unknown[]>(args: [...A, Options]

    This one isn't suitable for us since we want to have optional options, and it can be achieved by telling the compiler that we expect A or A with options:

    args: [...A, Options] | A
    

    Implementation:

    const wrapFunc = <A extends any[], R>(fn: (...args: A) => R) => {
      const wrappedFunc = (...args: [...A, Options] | A) => {
        const options: Options = 'force' in args[args.length - 1] ? args.pop() : {};
        fn(...(args.slice(-1) as A));
      };
    
      return wrappedFunc;
    };
    
    // const newFunc: (...args: [a: string] | [string, Options]) => void
    const newFunc = wrapFunc(func);
    

    This works as expected; however, the label of a is removed in the hinting when you also accept Options. To fix it, we should use labeled tuples as follows:

    const wrappedFunc = (...args: [...mainArgs: A, options: Options] | A) => {}
    

    This way hinting will be better:

    // const newFunc: (...args: [a: string] | [a: string, options: Options]) => void
    const newFunc = wrapFunc(func); 
    

    playground

    Note: You can also consider using the following approach, however, it may throw some Typescript errors depending on your tsconfig:

    const wrappedFunc = (...args: [...mainArgs: A, options?: Options]) => {}
    
    // const newFunc: (a: string, options?: Options) => voi
    const newFunc = wrapFunc(func);