-
Notifications
You must be signed in to change notification settings - Fork 12.6k
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
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; |
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 |
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. |
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. |
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 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. |
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 ```
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]; ```
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
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:
The text was updated successfully, but these errors were encountered: