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

Support inferring rest args that are not at the end of the function's args #39595

Closed
5 tasks done
dantman opened this issue Jul 14, 2020 · 9 comments · Fixed by #41544
Closed
5 tasks done

Support inferring rest args that are not at the end of the function's args #39595

dantman opened this issue Jul 14, 2020 · 9 comments · Fixed by #41544
Assignees
Labels
Bug A bug in TypeScript Fix Available A PR has been opened for this issue

Comments

@dantman
Copy link

dantman commented Jul 14, 2020

Search Terms

variadic infer rest

Suggestion

TypeScript 4.0 added support for "Variadic Tuple Types", the key relevant change being that spreads for tuple types can be done anywhere in a tuple, not just at the end. However it appears this does not extend to rest and infer.

It should be possible to infer an ...args that is not at the end of a function (callable?) type.

Use Cases

I use TypeScript API clients generated from Swagger/OpenAPI specs in React code by creating hooks to make the API calls in a React safe way. Naturally I want the to pass the API method args through the hook so the input args have the correct type.

However the API methods on these clients typically have something like an options?: FetchOptionsType at the end that shouldn't be part of the input args type. Options like these would be the purview of the hook itself, not relevant to the input arg. (...args: infer A, options? FetchOptionsType) => any seems like it would be the natural way to infer input args without the last arg, but does not work.

Examples

type ApiMethod = (...args: any[]) => Promise<void>;
// This does not work, even in TS 4.0
type ApiMethodArgs<T> = T extends (...args: infer A, options?: any) => any
  ? A
  : never;
// This would work but gives the wrong type
// type ApiMethodArgs<T> = T extends (...args: infer A) => any
//   ? A
//   : never;
// @note The following works so infer is valid when mixing rest/non-rest args but just doesn't work if rest args are not last
// type ApiMethodArgs<T> = T extends (options: any, ...args: infer A) => any
//   ? A
//   : never;

// External API method, all API methods follow 
async function getUser(id: string, options?: any): Promise<void> { }

function callApi<AM extends ApiMethod>(method: AM) {
    const commonApiOptions = {};
    return async (...args: ApiMethodArgs<AM>): Promise<void> => {
        await method(...args, commonApiOptions)
    }
}

callApi(getUser)('asdf');

https://www.typescriptlang.org/play/index.html?ts=4.0.0-beta#code/FAFwngDgpgBAghAlgWSiAFgewCYwLwwAUAdKQIYBOA5gM4BcMZAdmANoC6AlPgHwwAKFTAFtENKAB4AbpkTYeAbmAB6ZTAAq6MTGyYoNGE0wgYAd0wUA1gBoYUKVCYxET9QGUYAFmIAGUJFgEFDQsbDhqGgl1PgJ1OwAPEEdsAxJyCIYXADMoCnhbTAgQREwmGgB+BmYwbjw+auAYGHL4RpgGJntcpVUNLQNzAFcAG1xzKxgAI0GTKkQHAwxYUyEmKhhwaBU1TcCkVAwccNoomI0EpKYUolJiSlpMphy8uFr6lm2m5tbepo6uig9NQAASMST6sCymGGw0wphc63GlgMNEwzieuWcBikZGGcjM6EcMFE8QRMAo+hAyiMTAAtBSaCZ7gZpiYAFaDRk6PQ0JgAchMSOcWXJlMYEXFsDBMGGZEZn128H2ISOEVO+HOUESyVShWKpXojBYtluzMez3gbyNYE+TRacFt7UMAKUnwAotqKExcfB+ABJYkq7C2XHDX0B4RBgxQmFwmDAOVgJgAYxgWUGKf1TioaAAquIKIQ5AxGRQEQUiiUypVrZwGIIRGJJDI5HwAN4wAC+wGA6czVZgydDQQkcGQFx1SuCh3khEjM4YY+4bbaTWTBpM6+EwlKQQA8pWDRq252lF9RSBBl7GDQk6m0ncMlODqFjpExzw6wIhKJxNJZPIvAwCu57nmQphkIgJjzqED7MrYW47kw+6HmUnCrl2wDdsAQ4wkEhA5iA+a5JwhB8nK2BZHynAKEAA

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.
@IllusionMH
Copy link
Contributor

IllusionMH commented Jul 14, 2020

Variadic types allows to do that, but you need to use tuples for function arguments when you are inferring parameters instead of rest in arguments list.

type ApiMethodArgs<T> = T extends (...args: [...infer A, any]) => any
  ? A
  : never;

Example

@RyanCavanaugh RyanCavanaugh added Awaiting More Feedback This means we'd like to hear from more people who would be helped by this feature Suggestion An idea for TypeScript labels Jul 14, 2020
@ahejlsberg
Copy link
Member

Don't think any additional features are required to support this scenario. Here's how I'd write it (async stuff removed for clarity):

declare function getUser(id: string, options?: { x?: string }): string;

declare function getOrgUser(id: string, orgId: number, options?: { y?: number, z?: boolean }): void;

function callApi<T extends unknown[] = [], U = void>(method: (...args: [...T, object]) => U) {
    return (...args: [...T]) => method(...args, {});
}

callApi(getUser)('asdf');
callApi(getOrgUser)('asdf', 123);

// Errors as expected
callApi(getUser)();
callApi(getUser)(123);

callApi(getOrgUser)();
callApi(getOrgUser)('asdf');
callApi(getOrgUser)('asdf', '123');
callApi(getOrgUser)('asdf', '123', false);

Note that T has a [] default to ensure a very restrictive type is inferred if no inferences can be made for T. Also note the object type to represent the optional options (which presumably will be an object type with all optional properties).

@ahejlsberg
Copy link
Member

Link to example in playground.

Note that the example doesn't work with the nightly because of a small issue that I will fix shortly.

@dantman
Copy link
Author

dantman commented Jul 15, 2020

I could swear that the tuple version didn't work either when I tested it. But I guess it does.

That said, it would be nice if the args version was valid because this is valid and it doesn't make sense that the non-tuple version is valid when the rest is at the end but isn't valid at the start unless you switch to the tuple version.

type ApiMethodArgs<T> = T extends (options: any, ...args: infer A) => any
  ? A
  : never;

@ahejlsberg
Copy link
Member

@dantman In ECMAScript, only the last parameter can be a rest parameter, so this really is a request for a new ECMAScript feature.

@dantman
Copy link
Author

dantman commented Jul 15, 2020

@dantman In ECMAScript, only the last parameter can be a rest parameter, so this really is a request for a new ECMAScript feature.

It is not. The same restriction goes for array destructuring, only the last part of an array/tuple destructure can be a rest. But that is valid in TypeScript as of 4.0.

This is a type only change. It works with tuples now and should work the same with args.

@ahejlsberg
Copy link
Member

This is a type only change.

It would be an odd asymmetry to allow function types to have "middle rest" parameters, but not allow them in function declarations. Particularly since function declarations are often the origin of function types. I think our users have a fairly fundamental assumption that parameter lists of a function types can be directly transcribed into parameter lists of compatible function declarations, and that would no longer be true.

@benlesh
Copy link

benlesh commented Oct 23, 2020

@ahejlsberg, @DanielRosenwasser: Unfortunately, the recommended solution seems to be broken if you try to narrow the types in the spread arguments if those types have a generic... I encountered this trying to improve types in RxJS

Here is a playground showing the issue

And here is the code directly:

class Blah<T> {
  constructor(public readonly blah: T) {}
}

declare function f<S extends readonly [Blah<unknown>, ...Blah<unknown>[]]>(
  ...stringsAndNumber: readonly [...S, number]
): [...S, number];


const blah1 = new Blah('a');
const blah2 = new Blah([]);

const a = f(blah1, 1); // <ok> no error
const b = f(blah1, blah2, 1); // <ok> no error
const c = f(1); // <ok> error
const d = f(1, 2); // <ok> error
const e = f(blah1, blah2, 1, 2, 3); // <BAD> No error. <--------

EDIT: It's probably also worth noting that the .d.ts output is equally weird for the last three:

declare const c: [Blah<unknown>, ...(number | Blah<unknown>)[]];
declare const d: [Blah<unknown>, ...(number | Blah<unknown>)[]];
declare const e: [Blah<unknown>, ...(number | Blah<unknown>)[]];

@benlesh
Copy link

benlesh commented Oct 23, 2020

Apparently, the bug has nothing to do with generics... a simple type like string sees the same problem:

declare function f2<S extends readonly [string, ...string[]]>(
  ...stringsAndNumber: readonly [...S, number]
): [...S, number];


const a1 = f2('blah1', 1);
const b1 = f2('blah1', 'blah2', 1);
const c1 = f2(1);
const d1 = f2(1, 2);
const e1 = f2('blah1', 'blah2', 1, 2, 3); // should error but doesn't.

Playground link

@ahejlsberg ahejlsberg self-assigned this Nov 12, 2020
@ahejlsberg ahejlsberg added Bug A bug in TypeScript and removed Awaiting More Feedback This means we'd like to hear from more people who would be helped by this feature Suggestion An idea for TypeScript labels Nov 12, 2020
@ahejlsberg ahejlsberg added this to the TypeScript 4.2.0 milestone Nov 12, 2020
@typescript-bot typescript-bot added the Fix Available A PR has been opened for this issue label Nov 15, 2020
craigphicks added a commit to craigphicks/TypeScript-cph that referenced this issue May 13, 2022
 Changes to be committed:
	modified:   FIX_tuple-rest.md
	modified:   src/compiler/checker.ts
	new file:   tmp.cph.d/_aect.ts
	new file:   tmp.cph.d/_alect.ts
	renamed:    tests/cases/compiler/_cph1.ts -> tmp.cph.d/_cph1.ts
	renamed:    tests/cases/compiler/_vt2.ts -> tmp.cph.d/_vt2.ts

With entry condition for new code
```
    if (contextualType && isTupleType(contextualType)) {
```
Failing tests:
  80958 passing (5m)
  3 failing

  1)
         conformance tests
           conformance tests for tests/cases/conformance/types/tuple/variadicTuples2.ts
             Correct type/symbol baselines for tests/cases/conformance/types/tuple/variadicTuples2.ts:
     Error: New baseline created at tests/baselines/local/variadicTuples2.types
  2)
                conformance tests
           conformance tests for tests/cases/conformance/types/tuple/variadicTuples1.ts
             Correct errors for tests/cases/conformance/types/tuple/variadicTuples1.ts:
     Error: New baseline created at tests/baselines/local/variadicTuples1.errors.txt
  3)
         conformance tests
           conformance tests for tests/cases/conformance/types/tuple/variadicTuples1.ts
             Correct type/symbol baselines for tests/cases/conformance/types/tuple/variadicTuples1.ts:
     Error: New baseline created at tests/baselines/local/variadicTuples1.types

No difference in final result, only in intermediate types and error message.

```
$ diff -c6 tests/baselines/reference/variadicTuples1.types tests/baselines/local/variadicTuples1.types
*** tests/baselines/reference/variadicTuples1.types     2022-05-01 10:45:12.880934695 -0700
--- tests/baselines/local/variadicTuples1.types 2022-05-13 09:44:44.783651538 -0700
***************
*** 1394,1406 ****

  type Unbounded = [...Numbers, boolean];
  >Unbounded : [...number[], boolean]

  const data: Unbounded = [false, false];  // Error
  >data : [...number[], boolean]
! >[false, false] : [false, false]
  >false : false
  >false : false

  type U1 = [string, ...Numbers, boolean];
  >U1 : [string, ...number[], boolean]

--- 1394,1406 ----

  type Unbounded = [...Numbers, boolean];
  >Unbounded : [...number[], boolean]

  const data: Unbounded = [false, false];  // Error
  >data : [...number[], boolean]
! >[false, false] : [boolean, false]
  >false : false
  >false : false

  type U1 = [string, ...Numbers, boolean];
  >U1 : [string, ...number[], boolean]
```

```
$ diff -c6 tests/baselines/reference/variadicTuples2.types tests/baselines/local/variadicTuples2.types
*** tests/baselines/reference/variadicTuples2.types     2022-05-01 10:45:12.880934695 -0700
--- tests/baselines/local/variadicTuples2.types 2022-05-13 09:44:04.671647544 -0700
***************
*** 454,466 ****
  >1 : 1
  >'abc' : "abc"

  fn1([1, 'abc', true]);  // [string, boolean]
  >fn1([1, 'abc', true]) : [string, boolean]
  >fn1 : <T, U>(t: [...unknown[], T, U]) => [T, U]
! >[1, 'abc', true] : [number, string, boolean]
  >1 : 1
  >'abc' : "abc"
  >true : true

  declare function fn2<T, U>(t: [T, ...unknown[], U]): [T, U];
  >fn2 : <T, U>(t: [T, ...unknown[], U]) => [T, U]
--- 454,466 ----
  >1 : 1
  >'abc' : "abc"

  fn1([1, 'abc', true]);  // [string, boolean]
  >fn1([1, 'abc', true]) : [string, boolean]
  >fn1 : <T, U>(t: [...unknown[], T, U]) => [T, U]
! >[1, 'abc', true] : [number, string, true]
  >1 : 1
  >'abc' : "abc"
  >true : true

  declare function fn2<T, U>(t: [T, ...unknown[], U]): [T, U];
  >fn2 : <T, U>(t: [T, ...unknown[], U]) => [T, U]
***************
*** 484,496 ****
  >1 : 1
  >'abc' : "abc"

  fn2([1, 'abc', true]);  // [number, boolean]
  >fn2([1, 'abc', true]) : [number, boolean]
  >fn2 : <T, U>(t: [T, ...unknown[], U]) => [T, U]
! >[1, 'abc', true] : [number, string, boolean]
  >1 : 1
  >'abc' : "abc"
  >true : true

  // Repro from microsoft#39595
--- 484,496 ----
  >1 : 1
  >'abc' : "abc"

  fn2([1, 'abc', true]);  // [number, boolean]
  >fn2([1, 'abc', true]) : [number, boolean]
  >fn2 : <T, U>(t: [T, ...unknown[], U]) => [T, U]
! >[1, 'abc', true] : [number, string, true]
  >1 : 1
  >'abc' : "abc"
  >true : true

  // Repro from microsoft#39595
```
craigphicks added a commit to craigphicks/TypeScript-cph that referenced this issue May 13, 2022
 Changes to be committed:
	modified:   FIX_tuple-rest.md
	modified:   src/compiler/checker.ts
	new file:   tmp.cph.d/_aect.ts
	new file:   tmp.cph.d/_alect.ts
	renamed:    tests/cases/compiler/_cph1.ts -> tmp.cph.d/_cph1.ts
	renamed:    tests/cases/compiler/_vt2.ts -> tmp.cph.d/_vt2.ts

With entry condition for new code
```
    if (contextualType && isTupleType(contextualType)) {
```
Failing tests:
  80958 passing (5m)
  3 failing

  1)
         conformance tests
           conformance tests for tests/cases/conformance/types/tuple/variadicTuples2.ts
             Correct type/symbol baselines for tests/cases/conformance/types/tuple/variadicTuples2.ts:
     Error: New baseline created at tests/baselines/local/variadicTuples2.types
  2)
                conformance tests
           conformance tests for tests/cases/conformance/types/tuple/variadicTuples1.ts
             Correct errors for tests/cases/conformance/types/tuple/variadicTuples1.ts:
     Error: New baseline created at tests/baselines/local/variadicTuples1.errors.txt
  3)
         conformance tests
           conformance tests for tests/cases/conformance/types/tuple/variadicTuples1.ts
             Correct type/symbol baselines for tests/cases/conformance/types/tuple/variadicTuples1.ts:
     Error: New baseline created at tests/baselines/local/variadicTuples1.types

No difference in final result, only in intermediate types and error message.

```
$ diff -c6 tests/baselines/reference/variadicTuples1.types tests/baselines/local/variadicTuples1.types
*** tests/baselines/reference/variadicTuples1.types     2022-05-01 10:45:12.880934695 -0700
--- tests/baselines/local/variadicTuples1.types 2022-05-13 09:44:44.783651538 -0700
***************
*** 1394,1406 ****

  type Unbounded = [...Numbers, boolean];
  >Unbounded : [...number[], boolean]

  const data: Unbounded = [false, false];  // Error
  >data : [...number[], boolean]
! >[false, false] : [false, false]
  >false : false
  >false : false

  type U1 = [string, ...Numbers, boolean];
  >U1 : [string, ...number[], boolean]

--- 1394,1406 ----

  type Unbounded = [...Numbers, boolean];
  >Unbounded : [...number[], boolean]

  const data: Unbounded = [false, false];  // Error
  >data : [...number[], boolean]
! >[false, false] : [boolean, false]
  >false : false
  >false : false

  type U1 = [string, ...Numbers, boolean];
  >U1 : [string, ...number[], boolean]
```

```
$ diff -c6 tests/baselines/reference/variadicTuples2.types tests/baselines/local/variadicTuples2.types
*** tests/baselines/reference/variadicTuples2.types     2022-05-01 10:45:12.880934695 -0700
--- tests/baselines/local/variadicTuples2.types 2022-05-13 09:44:04.671647544 -0700
***************
*** 454,466 ****
  >1 : 1
  >'abc' : "abc"

  fn1([1, 'abc', true]);  // [string, boolean]
  >fn1([1, 'abc', true]) : [string, boolean]
  >fn1 : <T, U>(t: [...unknown[], T, U]) => [T, U]
! >[1, 'abc', true] : [number, string, boolean]
  >1 : 1
  >'abc' : "abc"
  >true : true

  declare function fn2<T, U>(t: [T, ...unknown[], U]): [T, U];
  >fn2 : <T, U>(t: [T, ...unknown[], U]) => [T, U]
--- 454,466 ----
  >1 : 1
  >'abc' : "abc"

  fn1([1, 'abc', true]);  // [string, boolean]
  >fn1([1, 'abc', true]) : [string, boolean]
  >fn1 : <T, U>(t: [...unknown[], T, U]) => [T, U]
! >[1, 'abc', true] : [number, string, true]
  >1 : 1
  >'abc' : "abc"
  >true : true

  declare function fn2<T, U>(t: [T, ...unknown[], U]): [T, U];
  >fn2 : <T, U>(t: [T, ...unknown[], U]) => [T, U]
***************
*** 484,496 ****
  >1 : 1
  >'abc' : "abc"

  fn2([1, 'abc', true]);  // [number, boolean]
  >fn2([1, 'abc', true]) : [number, boolean]
  >fn2 : <T, U>(t: [T, ...unknown[], U]) => [T, U]
! >[1, 'abc', true] : [number, string, boolean]
  >1 : 1
  >'abc' : "abc"
  >true : true

  // Repro from microsoft#39595
--- 484,496 ----
  >1 : 1
  >'abc' : "abc"

  fn2([1, 'abc', true]);  // [number, boolean]
  >fn2([1, 'abc', true]) : [number, boolean]
  >fn2 : <T, U>(t: [T, ...unknown[], U]) => [T, U]
! >[1, 'abc', true] : [number, string, true]
  >1 : 1
  >'abc' : "abc"
  >true : true

  // Repro from microsoft#39595
```

```
$ diff -c6 tests/baselines/reference/variadicTuples1.errors.txt tests/baselines/local/variadicTuples1.errors.txt
*** tests/baselines/reference/variadicTuples1.errors.txt        2022-05-01 10:45:12.880934695 -0700
--- tests/baselines/local/variadicTuples1.errors.txt    2022-05-13 09:44:42.575651323 -0700
***************
*** 33,45 ****
  tests/cases/conformance/types/tuple/variadicTuples1.ts(191,5): error TS2322: Type '[...T]' is not assignable to type '[...U]'.
    Type 'T' is not assignable to type 'U'.
      'T' is assignable to the constraint of type 'U', but 'U' could be instantiated with a different subtype of constraint 'readonly string[]'.
  tests/cases/conformance/types/tuple/variadicTuples1.ts(203,5): error TS2322: Type 'string' is not assignable to type 'keyof [1, 2, ...T]'.
    Type '"2"' is not assignable to type '"0" | "1" | keyof T[]'.
  tests/cases/conformance/types/tuple/variadicTuples1.ts(357,26): error TS2322: Type 'string' is not assignable to type 'number'.
! tests/cases/conformance/types/tuple/variadicTuples1.ts(397,7): error TS2322: Type '[false, false]' is not assignable to type '[...number[], boolean]'.
    Type at position 0 in source is not compatible with type at position 0 in target.
      Type 'boolean' is not assignable to type 'number'.

  ==== tests/cases/conformance/types/tuple/variadicTuples1.ts (20 errors) ====
      // Variadics in tuple types
--- 33,45 ----
  tests/cases/conformance/types/tuple/variadicTuples1.ts(191,5): error TS2322: Type '[...T]' is not assignable to type '[...U]'.
    Type 'T' is not assignable to type 'U'.
      'T' is assignable to the constraint of type 'U', but 'U' could be instantiated with a different subtype of constraint 'readonly string[]'.
  tests/cases/conformance/types/tuple/variadicTuples1.ts(203,5): error TS2322: Type 'string' is not assignable to type 'keyof [1, 2, ...T]'.
    Type '"2"' is not assignable to type '"0" | "1" | keyof T[]'.
  tests/cases/conformance/types/tuple/variadicTuples1.ts(357,26): error TS2322: Type 'string' is not assignable to type 'number'.
! tests/cases/conformance/types/tuple/variadicTuples1.ts(397,7): error TS2322: Type '[boolean, false]' is not assignable to type '[...number[], boolean]'.
    Type at position 0 in source is not compatible with type at position 0 in target.
      Type 'boolean' is not assignable to type 'number'.

  ==== tests/cases/conformance/types/tuple/variadicTuples1.ts (20 errors) ====
      // Variadics in tuple types
***************
*** 495,507 ****
      // Repro from microsoft#40235

      type Numbers = number[];
      type Unbounded = [...Numbers, boolean];
      const data: Unbounded = [false, false];  // Error
            ~~~~
! !!! error TS2322: Type '[false, false]' is not assignable to type '[...number[], boolean]'.
  !!! error TS2322:   Type at position 0 in source is not compatible with type at position 0 in target.
  !!! error TS2322:     Type 'boolean' is not assignable to type 'number'.

      type U1 = [string, ...Numbers, boolean];
      type U2 = [...[string, ...Numbers], boolean];
      type U3 = [...[string, number], boolean];
--- 495,507 ----
      // Repro from microsoft#40235

      type Numbers = number[];
      type Unbounded = [...Numbers, boolean];
      const data: Unbounded = [false, false];  // Error
            ~~~~
! !!! error TS2322: Type '[boolean, false]' is not assignable to type '[...number[], boolean]'.
  !!! error TS2322:   Type at position 0 in source is not compatible with type at position 0 in target.
  !!! error TS2322:     Type 'boolean' is not assignable to type 'number'.

      type U1 = [string, ...Numbers, boolean];
      type U2 = [...[string, ...Numbers], boolean];
      type U3 = [...[string, number], boolean];

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

Successfully merging a pull request may close this issue.

6 participants