Typescript: Support inferring rest args that are not at the end of the function's args

Created on 14 Jul 2020  路  9Comments  路  Source: microsoft/TypeScript

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:

  • [x] This wouldn't be a breaking change in existing TypeScript/JavaScript code
  • [x] This wouldn't change the runtime behavior of existing JavaScript code
  • [x] This could be implemented without emitting different JS based on the types of the expressions
  • [x] This isn't a runtime feature (e.g. library functionality, non-ECMAScript syntax with JavaScript output, etc.)
  • [x] This feature would agree with the rest of TypeScript's Design Goals.
Bug Fix Available

Most helpful comment

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

All 9 comments

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

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

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.

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;

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

@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.

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.

@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>)[]];

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

Was this page helpful?
0 / 5 - 0 ratings

Related issues

siddjain picture siddjain  路  3Comments

DanielRosenwasser picture DanielRosenwasser  路  3Comments

kyasbal-1994 picture kyasbal-1994  路  3Comments

Antony-Jones picture Antony-Jones  路  3Comments

blendsdk picture blendsdk  路  3Comments