How to wrap function and extend custom arguments with right type

54 views Asked by At

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!

1

There are 1 answers

0
wonderflame On BEST ANSWER

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);