Skip to content

Type JS's arguments object based on function parameters #29055

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

Open
4 tasks done
ethanresnick opened this issue Dec 16, 2018 · 9 comments
Open
4 tasks done

Type JS's arguments object based on function parameters #29055

ethanresnick opened this issue Dec 16, 2018 · 9 comments
Labels
In Discussion Not yet reached consensus Suggestion An idea for TypeScript

Comments

@ethanresnick
Copy link
Contributor

ethanresnick commented Dec 16, 2018

Search Terms

type "arguments" object parameters

Suggestion

Within a function x, the type of the arguments object should be Parameters<typeof x> & IArguments, with undefined removed from the type of any entries in Parameters<typeof x> if that parameter has a default value.

Use Cases

class A {
   fn(it: number) { return true; }
}

class B extends A {
  fn(it: number) {
    // this shouldn't error, because arguments must be at least length 1.
    // instead we get ERROR: Expected 1 arguments, but got 0 or more.
    return super.fn(...arguments); 
  }
}

Checklist

My suggestion meets these guidelines:

  • [?] This wouldn't be a breaking change in existing TypeScript/JavaScript code
  • This wouldn't change the runtime behavior of existing JavaScript code
  • This could be implemented without emitting different JS based on the types of the expressions
  • This isn't a runtime feature (e.g. library functionality, non-ECMAScript syntax with JavaScript output, etc.)
  • This feature would agree with the rest of TypeScript's Design Goals.
@weswigham weswigham added Suggestion An idea for TypeScript In Discussion Not yet reached consensus labels Dec 17, 2018
@DanielRosenwasser
Copy link
Member

What about using rest arguments? The following typechecks:

class A {
   fn(it: number) { return true; }
}

class B extends A {
  fn(...args: [number]) {
    // this shouldn't error, because arguments must be at least length 1.
    // instead we get ERROR: Expected 1 arguments, but got 0 or more.
    return super.fn(...args); 
  }
}

@ethanresnick
Copy link
Contributor Author

ethanresnick commented Dec 18, 2018

What about using rest arguments?

Rest arguments work, but:

  1. I don't think they can be used if any of the arguments need default values;

  2. They make the code less readable, since I can't give each argument its own name in the signature (which effects IDE popup docs too)

For example, my real subclass's function signature looked more like:

fn(
  data: PartialDataProps<X> | PartialDataProps<X>[] = {}, 
  options: Parameters<typeof A["fn"]>[1] = {}
) { return true; }

Holding aside the default values, having the arguments named data and options was much clearer than:

fn(...args: [
  PartialDataProps<X> | PartialDataProps<X>[],
  Parameters<typeof A["fn"]>[1]
]) { return true; }

@ethanresnick
Copy link
Contributor Author

ethanresnick commented Dec 18, 2018

Also, I just noticed that the Parameters adds undefined to the type of each argument that has a default value, which makes sense. But, for my proposal that undefined shouldn't be present in the type of arguments's entries, as the default value will have been applied inside the function. I've updated the OP accordingly.

@chrmarti
Copy link

The strictBindCallApply feature makes the following show an error for passing arguments where an array is expected. That might be fixed by what is being discussed here as well.

function one(a: string, b: number) {

}

function two(a: string, b: number) {
	one.apply(null, arguments);
}

@ElianCordoba
Copy link

Same problem here, I tried this but it didn't work either

...
testFn(a, b, c) {
  const args = [...arguments].slice(0, 3);
  testFn(...args);
}
...

@ethanresnick
Copy link
Contributor Author

This bit me again today, where I had a function like:

function wrapper(a: A, b: B, c: C, d: D, e: E) {
  // callthrough to the wrapped function, which should have same signature
  if(!someCondition) {
    return wrapped(...arguments);
  } else { 
    // do something clever
  }
} 

Using rest parameters for one or both functions would've made their signatures basically unreadable. And every way I could think of to type-safely maintain the type of the arguments list in only one place (and then ig cast ...arguments to that before the call) was a mess.

This example is similar to @chrmarti's, although with wrapper/wrapped I'm trying to show that, in my case, the signatures didn't just coincidentally match (and I wasn't dealing with bind/apply.

@ethanresnick
Copy link
Contributor Author

I've also tried to work around this by manually casting arguments to Parameters<typeof fn>, but that gives an error:

function wrapper() {
  // Errror: Type alias 'Args' circularly references itself.
  type Args = Parameters<typeof wrapper>;
  return wrapped(...(arguments as Args)); 
}

@n9
Copy link

n9 commented Apr 28, 2021

@ethanresnick For complex signatures, it will currently hit: #43731

@nicolo-ribaudo
Copy link

nicolo-ribaudo commented May 29, 2024

This now works:

type ToArrayLike<T extends unknown[]> = { [K in (keyof T) & `${bigint}`]: T[K] }
type Arguments<Fn extends (...args: any) => any> = IArguments & ToArrayLike<Parameters<Fn>>;

function fn(a: number, b: string) {
    let args = arguments as Arguments<typeof fn>;

    let _a = args[0];
    // ^?
}

(it's be great if TS could automatically infer arguments as having that type)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
In Discussion Not yet reached consensus Suggestion An idea for TypeScript
Projects
None yet
Development

No branches or pull requests

7 participants