Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Compiler incorrectly reports parameter/call target signature mismatch when using the spread operator #4130

Closed
smashdevcode opened this issue Aug 3, 2015 · 57 comments · Fixed by #24897
Assignees
Labels
Bug A bug in TypeScript Fixed A PR has been merged for this issue

Comments

@smashdevcode
Copy link

The compiler incorrectly reports a parameter/call target signature mismatch when using the spread operator. Pasting this code into the TypeScript Playground:

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

produces this error:

Supplied parameters do not match any signature of call target.
@danquirk danquirk added the Bug A bug in TypeScript label Aug 3, 2015
@tjoskar
Copy link

tjoskar commented Aug 7, 2015

And the same is for when args is of any-type

function foo(x: number, y: number, z: number) { }

function bar(...args) {
    foo(...args); // Supplied parameters do not match any signature of call target.
}

@mhegazy
Copy link
Contributor

mhegazy commented Oct 6, 2015

this is currently by design. but we should reconsider it.

@mhegazy mhegazy added this to the TypeScript 2.0 milestone Oct 6, 2015
@sandersn
Copy link
Member

@smashdevcode, @tjoskar, I'm looking for some real-world uses of this feature. Specifically, do you expect to spread arrays or tuples (or both maybe)? Here are couple of toy examples:

//// arrays ////
var a1: number[] = [1,2];
var a2: number[] = [1,2,3];
var a3: number[] = [];
function twoNumbersOrSo(n?: number, m?: number) {
  return (n || -1) * (m || -1);
}
function howManyNumbersLessOne(n?: number, ...ns: number[]) {
    return ns.length;
}

//// tuples ////
var t1: [number, string] = [1, "foo"];
var t2: [number, string, string] = [1, "foo", "bar"];
function veryTraditional(n: number, s: string) {
    for (let i = 0; i < n; i++) {
        console.log(s);
    }
}
function moreInteresting(n: number, s: string, ...rest: string[]) {
    veryTraditional(n, s);
    console.log(rest);
}

Now any of a1,a2,a3 can be applied to the first two functions, and any of t1,t2 can be applied to the second two.

Notice that with arrays:

  1. Have a single type, so parameters all have to be the same type. (I guess this could be any.)
  2. The length of arrays is not known at compile time, so the parameters have to be optional. And the spread argument has to be the last (like today).

With tuples:

  1. The value will have to have a type annotation at some point. Otherwise it will be interpreted as an array (eg (number | string)[] not [number, number, string]). In toy code this erases any savings in brevity, but it might be OK in a real project that already defines lots of interfaces.
  2. The length is known ahead of time, so a tuple can be applied to any kind of parameter list.

@tjoskar
Copy link

tjoskar commented Oct 27, 2015

Hi @sandersn,

Sorry for late response. I totally missed your reply.
I only use arrays with one datatype so the tuple-version is not very interesting for me.
However I think that the spread operation in function calls should accepts any iterable object. So it should work with eg. map and set, and in these cases we will need to create an interface that works with the spread operator even if the data contains multiple datatypes (eg. new Set().add(1).add('string')). But that is maybe another ticket?

Example with Set and the spread operation (works in Chrome 46)

function foo(...args) {
    console.log(...args);
}
var bar = new Set().add(1).add(2);

foo(...bar); // 1, 2

@sandersn
Copy link
Member

Yeah, it's a separate issue. Typescript already supports arrays as the type for rest parameters, which is the thing that would need to be made generic. We're just discussing where spread args could be used.

Do you want to pass your homogenous arrays/sets to functions with rest-parameters or ones with a fixed number of arguments? I'd like more detail on the thing you would be calling.

@tjoskar
Copy link

tjoskar commented Oct 28, 2015

First of all, I have playing around with the spread operator this morning and most of my use cases works fine in the latest version of typescript typescript@1.8.0-dev.20151027 and on http://www.typescriptlang.org/Playground. So this is not a big deal anymore.

However,
I don't like the idea to make all parameters optional just to be able to use
spread operator BUT I understand the problem; the length of arrays is not known at compile time so
it is impossible to check the parameters at compile time.

However, I think it would be nice if the parameters don't need to be optional and that the compiler
only check that "spread-variable" is of right type eg. number[]

function fun1(x: number, y: number, z: number) {
    return x+y+z;
}

let arr1 = [1, 2, 3];
let arr2 = [1, 2];

fun1(...arr1);
fun1(...arr2); // OK since `arr2` is number[]
fun1(1, 2); // Should cause an error

Is that possible?

The following code works in the current version of typescript, which it didn't when this issue was addressed.

function fun1(...num) {
    return Math.max(...num);
}

function fun2(a1, ...num) {
    return Math.max(...num);
}

function fun3(a1, ...num) {
    return fun1(...num);
}

let arr = [1, 2, 3];

if (Math.random() < .5) {
    arr.push(1);
}

fun1(...arr);
fun2('first param', ...arr);
fun3('first param', ...arr);

Maybe a more realistic example (that also works nowadays):

const doSomeWork = ({title, genre, runtime}) => { console.log(title, genre, runtime); };

function fun1(...num) {
    num.forEach(doSomeWork);
}

const data = [{
    title: 'title',
    genre: ['action', 'drama'],
    runtime: 100
}];

fun1(...data);

@LordZardeck
Copy link

👍 Having the same issue, with the specific use case of the Date constructor:

let dateNumberArray: Array<number> = [2015,11,11];
let myNewDate = new Date(...dateNumberArray);

@envygeeks
Copy link

For us we use spreads to allow users to configure argv requirements in a base app without the need to build their own helpers. For example:

{
  "yargv": [
    [
      "path",
      {
        "demand": true,
        "describe": "Source",
        "default": ".",
        "alias": "p"
      }
    ]
  ]
}
get appArgs() : { yargv: Array<Array<any>>, chdir: Array<boolean | string> } {
  return require(
    "../args.json"
  )
}

argv() : Promise<{}> {
  return new Promise((resolve) => {
    this.appArgs.yargv.forEach(v => yargs.option(...v))
    return resolve(yargs.usage("$0 [args]").help("h").
      alias("h", "help").argv)
  })
}

@Blasz
Copy link
Contributor

Blasz commented Jun 30, 2016

A real world example using the combineLatest function of https://github.com/ReactiveX/rxjs where I want to preserve the this value when calling projectFn using an arrow function and use rest parameters to propagate the arguments.

getCombination() {
    return this.firstObservable
      .combineLatest(this.secondObservable, (...args) => this.projectFn(...args)));
  }

Currently I have to do

getCombination() {
    return this.firstObservable
      .combineLatest(this.secondObservable, (...args) => this.projectFn.apply(this, args)));
  }

@ejmurra
Copy link

ejmurra commented Jul 5, 2016

FWIW, another real world example is trying to deep merge an array of objects using merge from lodash:

import { merge } from 'lodash';

...
return merge(...arrayOfObjects);

MichalZalecki added a commit to MichalZalecki/policeman that referenced this issue Aug 21, 2016
@mhegazy mhegazy modified the milestones: Future, TypeScript 2.1 Sep 29, 2016
@adamreisnz
Copy link

Here's another real world use case; we're trying to refactor code that looks like this:

return Observable
      .forkJoin(a, b)
      .map(([current, past]) => this.mergeData(current, past));

Into the slightly more elegant:

return Observable
      .forkJoin(a, b)
      .map(data => this.mergeData(...data));

But using the spread operator triggers the signature mismatch error.

@mhegazy mhegazy added this to the TypeScript 3.0 milestone Apr 26, 2018
@janproch
Copy link

janproch commented May 1, 2018

Another real world example, in which correct handling of spread operator in function call will help a lot:
I have functions generated by NSwagStudio - API generator for C# WebApi, which have the same parameters (GET parameters are defined in structure)

The generated function looks like following:

export interface ITableApiClient {
    getTableInfo(objnam: string | null, objsch: string | null | undefined, dbnam: string | null, conid: number | undefined): Promise<FileResponse | null>;
    otherFunction(objnam: string | null, objsch: string | null | undefined, dbnam: string | null, conid: number | undefined): Promise<string | null>;
}

I want to call it with syntax

   get tableIdentification() {
       return [this.name, this.schema, this.databaseName, this.serverConnectionId];
   }
...
   return apiClient.getTableInfo(...this.tableIdentification);

   // this doesn't help, still compile-time error
   // return apiClient.getTableInfo(...(this.tableIdentification as any));

because there are more functions with exactly the same parameters (because they are generated from the same parameters class defined in C# backend). Now I have to copy body of property tableIdentification n-times into each usage

@darekf77
Copy link

@smashdevcode For me solution was to add @ts-ignore

function foo(x: number, y: number, z: number) { }
var args = [0, 1, 2];
// @ts-ignore
foo(...args);

example here: https://stackblitz.com/edit/typescript-ecymei?embed=1&file=index.ts

@tjoskar
Copy link

tjoskar commented Mar 20, 2019

@darekf77, or if you say that args is a tuple:

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

ts-playground

@Giwayume
Copy link

Giwayume commented Apr 8, 2019

@darekf77, or if you say that args is a tuple:

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

This works for me when I compile typescript in the command line, but not when Visual Studio Code validates the typescript while I'm typing. I'm not sure what the disconnect is here.

@TidyIQ
Copy link

TidyIQ commented Apr 24, 2019

I see it's been over a year since jayphelps mentioned that this would be fixed in an upcoming release, but this issue still occurs. Is there any update on this? I know I could use @ts-ignore however this removes ALL typescript messages for the function so isn't really an appropriate solution.

@Giwayume
Copy link

@TidyIQ as mentioned by tjoskar, typecast your spread array as a tuple.

@TidyIQ
Copy link

TidyIQ commented Apr 24, 2019

The array isn't fixed-length so that won't work.

@Giwayume
Copy link

A typecast will work as far as the compiler is concerned, even if it is innacurate. I agree it's not the best solution.

@tjoskar
Copy link

tjoskar commented Apr 25, 2019

(This question should probably be moved to stack overflow (or similar))

@TidyIQ, if the array doesn't have a fix length, the compiler can not tell if the array is suitable or not.

function foo(x: number, y: number, z: number) { }
const args = arrayOfSomeLength;
foo(...args); // Error: The compiler do not know if there is 3 or 2 or 1 or 50 elements in the array. 

So if the array is of dynamic length the arguments should be that as well:

function foo(...args: number[]) { }
const args = arrayOfSomeLength;
foo(...args);

Playground

@TidyIQ
Copy link

TidyIQ commented Apr 25, 2019

Unfortunately that won't work either as some of my args are fixed length.

Perhaps if I just post the code you might see the issue.

interface IF_Object {
  [key: string]: object;
}
//  VALUE SHOULD BE REQUIRED BUT MADE OPTIONAL TO BYPASS TYPESCRIPT ERROR
interface IF_SetKV {
  (key: string, value?: any): Function;
}
//  VALUE SHOULD BE REQUIRED BUT MADE OPTIONAL TO BYPASS TYPESCRIPT ERROR
interface IF_CreateState {
  (key: string, value?: any, ...more: string[]): Function;
}

const setKV: IF_SetKV = (key, value) => (object: IF_Object = {}) => ({
  ...object,
  [key]: value
});

const createState: IF_CreateState = (key, value, ...more) => (
  object: IF_Object = {}
) =>
  more.length === 0
    ? setKV(key, value)(object)
    : setKV(key, createState(value, ...more)(object[key]))(object);

// TYPESCRIPT ERROR OCCURS HERE. CAN ONLY BE REMOVED WITH TS@IGNORE
const newState = createState(...stateList, action.payload.value)(reduced);

@tjoskar
Copy link

tjoskar commented Apr 25, 2019

@TidyIQ, I'm not sure I follow. What happens if stateList is an empty array? In that case will action.payload.value be passed as the key?

Should not this work:

const createState = (...args: string[]) => (object: IF_Object = {}) => {
  const [key, value, ...rest] = args;
  if (rest.length === 0) {
    setKV(key, value)(object)
  } else {
    setKV(key, createState(value, ...rest)(object[key]))(object);
  }
}

@mateja176
Copy link

Cast to spread value as the ParameterType of the function you are passing arguments to.

const add = (a: number, b: number) => a + b

const values = {
  a: 1,
  b: 2,
}

type AddParams = Parameters<typeof add>

add(...(values) as AddParams)

@jpravetz
Copy link

@mateja176 This also seems to work and might better suit some use cases.

add(...(values as [number,number]))

or

add(...(values as [any,any]))

@loilo
Copy link

loilo commented Feb 5, 2020

I use a helper type for this to handle arbitrary arrays without having to type them explicitly:

type NonEmptyArray<T extends any[]> = T extends (infer U)[]
  ? [U, ...U[]]
  : never

It can be used like this:

add(...values as NonEmptyArray<typeof values>)
Detailed explanation for beginners

The following dissection of the NonEmptyArray type explains how it works in detail:

# Part Explanation
(1) type NonEmptyArray The name of the helper type
(2) <T The helper type takes a generic type parameter T.
(3) extends any[]> To be accepted by the type checker, that type parameter T must be some array type.
(4) T extends (infer U)[] ? Our helper type is a conditional type which checks whether T actually is an array type.
We have declared T as an array type in (3), so this condition always passes, but using the conditional allows us to let TypeScript infer the type that the T array is made of, and we call that type U.
(5) [U, ...U[]] Now we can assemble the resulting type: an array where the first entry is of type U and the remaining (0 or more) entries are also of type U.
Because of this specific tuple notation, TypeScript is aware that there is at least one item.
(6) : never This is just needed to syntactically complete the conditional. Remember: the conditional is just a trick to extract the U type, and it always passes. Therefore, we can safely ignore the "else" branch by yielding never.

Now if we do this...

const values = [1,2,3]
add(...values as NonEmptyArray<typeof values>)

...the following will happen:

  • typeof values is the type which TypeScript inferred for the values array, which is an array of numbers: number[].
  • That means, we pass number[] as T. (2/3)
  • T is indeed an array type, so, we can infer U from it, which is number. (4)
  • Now that we know U is a number, we yield the type [number, ...number[]]. (5)

@avshyz
Copy link

avshyz commented Jun 23, 2020

@tjoskar change your code to

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

(playground)
And the error's back.

Surprisingly, if you were to change the last line to foo(3, ...args); - there will be no error.

@Haaxor1689
Copy link

Haaxor1689 commented Sep 10, 2020

I feel like this still isn't working. Here is my example

onSetValue={(...args: Parameters<typeof props.onSetValue>) => {
    setLanguage(null);
    props.onSetValue(...args); // Expected 2 arguments, but got 0 or more.
  }}

It shouldn't really matter what is the type of props.onSetValue, because I just take the parameters type and pass it to the function which I got the the type from and it still gives the Expected 2 arguments, but got 0 or more. error.

Playground link

@mightyiam
Copy link

Here is a reduced form of @Haaxor1689's example:
Playground link

@raminious
Copy link

raminious commented Nov 26, 2020

I still can't get it working

this is my temporary workaround

class Board {
  private events: Events

  public on(...args: Parameters<this['events']['on']>) {
    this.events.on.call(this.events, ...args)
  }
}

@IgnusG
Copy link

IgnusG commented Dec 16, 2020

I feel like this is still a problem (just encountered this). Is there another open issue for this, or would it be relevant to re-open this one?

@aliatsis
Copy link

@ahejlsberg this issue either persists or there is a regression in the latest TS version.
The comment above (#4130 (comment)) demonstrates it perfectly.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Bug A bug in TypeScript Fixed A PR has been merged for this issue
Projects
None yet