Repository navigation
Allow rest elements to not be the last item in tuple types #25717
Description
Search Terms
rest element type tuple last
Related:
Suggestion
As of #24897, we can now use rest types in tuples to type argument lists. This is very nice since it allows us to easily type a couple of common methods. However there's still one big offender, which are the NodeJS callback style API methods. These generally accept a couple of arguments, with the callback method being the last one. Borrowing from the example in the RC-3.0 docs:
function nodeJsCallbackAPI<TReturn>(callback: (err: Error | undefined, result: TReturn) => void);
function nodeJsCallbackAPI<T1, TReturn>(arg1: T1, callback: (err: Error | undefined, result: TReturn) => void);
function nodeJsCallbackAPI<T1, T2, TReturn>(arg1: T1, arg2: T2, callback: (err: Error | undefined, result: TReturn) => void);
function nodeJsCallbackAPI<T1, T2, T3, TReturn>(arg1: T1, arg2: T2, arg3: T3, callback: (err: Error | undefined, result: TReturn) => void);
// AHH, how many overloads is enough?
function nodeJsCallbackAPI(...args: any[]) {
// do work, call callback!
}While rest parameters need to be the last in JavaScript, this limitation doesn't have to apply to the type system in TypeScript. I would love to see the ability for rest types to appear anywhere in a tuple type, e.g. like this:
type T1 = [...string[], number]; // any amount of strings, followed by a number. (length >= 1)
type T2 = [number, ...string[], number]; // a number, followed by any amount of strings, followed by a number. (length >= 2)
type T3 = [...string[], number?]; // any amount of strings, optionally followed by a number. (length >= 0)
type T4 = [number, ...string[], number?]; // a number, followed by any amount of strings, optionally followed by a number. (length >= 1)
// bonus points, not sure if that is logical or even well-defined, when there are overlapping types:
type T5 = [number?, ...string[], number?]; // an optional number, followed by any amount of strings, optionally followed by a number. (length >= 0)
type T6 = [...string[], number, ...string[]]; // any amount of strings, followed by a number, followed by any amount of strings. (length >= 1)
// weird thoughts, do these make sense?
type T7 = [...any[], number, (number | string)?]; // Could be anything, followed by [number] or [number, number] or [number, string]...
type T8 = [...any[], number, ...any[]]; // anything, but there must be a numberUse Cases
Typing the promisify and callbackify methods and similar patterns. Currently they look like this monster, which is inherently incomplete:
https://github.com/DefinitelyTyped/DefinitelyTyped/blob/91f8a6b7d1ef61cc543d0901dcc004b846283345/types/node/index.d.ts#L6275
export function callbackify(fn: () => Promise<void>): (callback: (err: NodeJS.ErrnoException) => void) => void;
export function callbackify<TResult>(fn: () => Promise<TResult>): (callback: (err: NodeJS.ErrnoException, result: TResult) => void) => void;
export function callbackify<T1>(fn: (arg1: T1) => Promise<void>): (arg1: T1, callback: (err: NodeJS.ErrnoException) => void) => void;
export function callbackify<T1, TResult>(fn: (arg1: T1) => Promise<TResult>): (arg1: T1, callback: (err: NodeJS.ErrnoException, result: TResult) => void) => void;
export function callbackify<T1, T2>(fn: (arg1: T1, arg2: T2) => Promise<void>): (arg1: T1, arg2: T2, callback: (err: NodeJS.ErrnoException) => void) => void;
export function callbackify<T1, T2, TResult>(fn: (arg1: T1, arg2: T2) => Promise<TResult>): (arg1: T1, arg2: T2, callback: (err: NodeJS.ErrnoException, result: TResult) => void) => void;
export function callbackify<T1, T2, T3>(fn: (arg1: T1, arg2: T2, arg3: T3) => Promise<void>): (arg1: T1, arg2: T2, arg3: T3, callback: (err: NodeJS.ErrnoException) => void) => void;
export function callbackify<T1, T2, T3, TResult>(fn: (arg1: T1, arg2: T2, arg3: T3) => Promise<TResult>): (arg1: T1, arg2: T2, arg3: T3, callback: (err: NodeJS.ErrnoException, result: TResult) => void) => void;
export function callbackify<T1, T2, T3, T4>(fn: (arg1: T1, arg2: T2, arg3: T3, arg4: T4) => Promise<void>): (arg1: T1, arg2: T2, arg3: T3, arg4: T4, callback: (err: NodeJS.ErrnoException) => void) => void;
export function callbackify<T1, T2, T3, T4, TResult>(fn: (arg1: T1, arg2: T2, arg3: T3, arg4: T4) => Promise<TResult>): (arg1: T1, arg2: T2, arg3: T3, arg4: T4, callback: (err: NodeJS.ErrnoException, result: TResult) => void) => void;
export function callbackify<T1, T2, T3, T4, T5>(fn: (arg1: T1, arg2: T2, arg3: T3, arg4: T4, arg5: T5) => Promise<void>): (arg1: T1, arg2: T2, arg3: T3, arg4: T4, arg5: T5, callback: (err: NodeJS.ErrnoException) => void) => void;
export function callbackify<T1, T2, T3, T4, T5, TResult>(fn: (arg1: T1, arg2: T2, arg3: T3, arg4: T4, arg5: T5) => Promise<TResult>): (arg1: T1, arg2: T2, arg3: T3, arg4: T4, arg5: T5, callback: (err: NodeJS.ErrnoException, result: TResult) => void) => void;
export function callbackify<T1, T2, T3, T4, T5, T6>(fn: (arg1: T1, arg2: T2, arg3: T3, arg4: T4, arg5: T5, arg6: T6) => Promise<void>): (arg1: T1, arg2: T2, arg3: T3, arg4: T4, arg5: T5, arg6: T6, callback: (err: NodeJS.ErrnoException) => void) => void;
export function callbackify<T1, T2, T3, T4, T5, T6, TResult>(fn: (arg1: T1, arg2: T2, arg3: T3, arg4: T4, arg5: T5, arg6: T6) => Promise<TResult>): (arg1: T1, arg2: T2, arg3: T3, arg4: T4, arg5: T5, arg6: T6, callback: (err: NodeJS.ErrnoException, result: TResult) => void) => void;
export function promisify<TCustom extends Function>(fn: CustomPromisify<TCustom>): TCustom;
export function promisify<TResult>(fn: (callback: (err: Error | null, result: TResult) => void) => void): () => Promise<TResult>;
export function promisify(fn: (callback: (err: Error | null) => void) => void): () => Promise<void>;
export function promisify<T1, TResult>(fn: (arg1: T1, callback: (err: Error | null, result: TResult) => void) => void): (arg1: T1) => Promise<TResult>;
export function promisify<T1>(fn: (arg1: T1, callback: (err: Error | null) => void) => void): (arg1: T1) => Promise<void>;
export function promisify<T1, T2, TResult>(fn: (arg1: T1, arg2: T2, callback: (err: Error | null, result: TResult) => void) => void): (arg1: T1, arg2: T2) => Promise<TResult>;
export function promisify<T1, T2>(fn: (arg1: T1, arg2: T2, callback: (err: Error | null) => void) => void): (arg1: T1, arg2: T2) => Promise<void>;
export function promisify<T1, T2, T3, TResult>(fn: (arg1: T1, arg2: T2, arg3: T3, callback: (err: Error | null, result: TResult) => void) => void): (arg1: T1, arg2: T2, arg3: T3) => Promise<TResult>;
export function promisify<T1, T2, T3>(fn: (arg1: T1, arg2: T2, arg3: T3, callback: (err: Error | null) => void) => void): (arg1: T1, arg2: T2, arg3: T3) => Promise<void>;
export function promisify<T1, T2, T3, T4, TResult>(fn: (arg1: T1, arg2: T2, arg3: T3, arg4: T4, callback: (err: Error | null, result: TResult) => void) => void): (arg1: T1, arg2: T2, arg3: T3, arg4: T4) => Promise<TResult>;
export function promisify<T1, T2, T3, T4>(fn: (arg1: T1, arg2: T2, arg3: T3, arg4: T4, callback: (err: Error | null) => void) => void): (arg1: T1, arg2: T2, arg3: T3, arg4: T4) => Promise<void>;
export function promisify<T1, T2, T3, T4, T5, TResult>(fn: (arg1: T1, arg2: T2, arg3: T3, arg4: T4, arg5: T5, callback: (err: Error | null, result: TResult) => void) => void): (arg1: T1, arg2: T2, arg3: T3, arg4: T4, arg5: T5) => Promise<TResult>;
export function promisify<T1, T2, T3, T4, T5>(fn: (arg1: T1, arg2: T2, arg3: T3, arg4: T4, arg5: T5, callback: (err: Error | null) => void) => void): (arg1: T1, arg2: T2, arg3: T3, arg4: T4, arg5: T5) => Promise<void>;
export function promisify(fn: Function): Function;Examples
With this functionality in place, we could type the above methods like this:
// This is just a Type-Script internal overload, rest arguments should be allowed to appear whereever they may appear in tuple types
function nodeJsCallbackAPI<TArgs extends any[], TReturn>(...args: TArgs, callback: (err: Error | undefined, result: TReturn) => void);
// This is the "official"/external overload. Therefore we cannot use rest arguments as the first argument and need to split the callback arg manually
function nodeJsCallbackAPI(...args: any[]) {
// split args from callback, do more work
}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. new expression-level syntax)
Activity
mhegazy commentedon Jul 17, 2018
Duplicate of #1360
AlCalzone commentedon Jul 17, 2018
Mohamed Hegazy (@mhegazy) I'm not sure how it looks like "under the hood", but it seems like #1360 is only concerned with the function signatures.
This issue also includes tuple types, which have just gotten the ability to include rest types. Do you count that part of my proposal as part of #1360 too?
mhegazy commentedon Jul 17, 2018
A tuple type can represent a function argument list, so both need to support the same features. assignablity rules are also the same.
typescript-bot commentedon Jul 27, 2018
Automatically closing this issue for housekeeping purposes. The issue labels indicate that it is unactionable at the moment or has already been addressed.