variadic infer rest
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.
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.
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
My suggestion meets these guidelines:
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;
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.
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):
Note that
Thas a[]default to ensure a very restrictive type is inferred if no inferences can be made forT. Also note theobjecttype to represent the optional options (which presumably will be an object type with all optional properties).