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 number
Use 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)