17

I am trying to use the spread-operator on a typescript-function call like this:

function foo(x: number, y: number, z: number) {
  console.log(x + y + z);
}
const args = [0, 1, 2];
foo(...args);

But on compilation, I get the error: "A spread argument must either have a tuple type or be passed to a rest parameter" (TS2556). What am I doing wrong?

Addendum: How can I approach the problem when my argument is a dynamic array, as in

const args = new Array(3).map(() => Math.random());
Martin J.H.
  • 2,085
  • 1
  • 22
  • 37

5 Answers5

22

Edit for dynamically generated args:

Option one: use type assertion if you are sure args will always be 3 elements tuple.

const args = new Array(3).map(() => Math.random()) as [number, number, number];

Option two, define foo to accept rest parameter:

function foo(...args: number[]) {
  console.log(args[0] + args[1] + args[2]);
}
const args = new Array(3).map(() => Math.random());
foo(...args);

Old answer for predefined args

You can assert args as const:

const args = [0, 1, 2] as const;

Playground

Or define args as tuple as the error suggested:

const args: [number, number, number] = [0, 1, 2];

Playground

This is to guarantee that number / type of elements in args always match what's required by the function parameters.

Psidom
  • 209,562
  • 33
  • 339
  • 356
  • Thank you! Unfortunately, it turned out that my minimal example was too minimal - my initialization of the args array is more complex -- I it is truly an array (see edit). But your explanation already helps me understanding the underlying issue -- I guess, since Arrays are mutable, there is no easy way to statically guarantee that the array-size matches, right? – Martin J.H. Aug 22 '21 at 18:40
  • Yeah if you can't guarantee the input size of `args` then you need to define function parameters to be rest type so it also accepts indefinite number of parameters. – Psidom Aug 22 '21 at 18:43
5

You can use const assertion to get rid of the problem. Because the function foo expects exactly 3 parameters:

function foo(x: number, y: number, z: number) {
  console.log(x + y + z);
}

const args = [0, 1, 2] as const;
foo(...args);
AliN11
  • 2,387
  • 1
  • 25
  • 40
1

Arguments don’t work in that way, this is how you should you them args

function multiply(n, ...m) {
    return m.map((x) => n * x);
}
// 'a' gets value [10, 20, 30, 40]
const a = multiply(10, 1, 2, 3, 4);
1

For dynamically inferred param sizes, the as const method doesn't work.

Another solution is to use the array resulting from ...args in an .apply() call. Typescript doesn't complain about this:

export class CustomError<
  ErrorCode extends keyof typeof CustomError.codes,
  ErrorParams extends Parameters<typeof CustomError.codes[ErrorCode]>
> extends Error {
  constructor(public code: ErrorCode, ...args: ErrorParams) {
    super(CustomError.codes[code].apply(null, args))
  }

  static codes = {
    invalidEmail (email: string) {
      return `Email ${email} is invalid`
    },
    invalidPasswordConfirm: (a: string, b: string) {
      return `Password ${a} and confirm ${b} are different`
    }
  }
}

new CustomError('invalidEmail', 'aaa@bbb')
// ^ CustomError(code: "invalidEmail", email: string)
new CustomError('invalidPasswordConfirm', 'first', 'second')
// ^ CustomError(code: "invalidPasswordConfirm", a: string, b: string)
Christophe Marois
  • 6,471
  • 1
  • 30
  • 32
  • I believe your answer only works because `.apply(thisArg, args)` is not strongly typed, for historic reasons, unless you use the `strictBindCallApply` option. – Martin J.H. Jan 18 '23 at 20:38
-3

Whoever suffers from A spread argument must either have a tuple type or be passed to a rest parameter., you might want to check whether typescript-parser is install in your devDependencies.

Tien Lin
  • 1,338
  • 1
  • 11
  • 6