Skip to content
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

Allow rest elements to not be the last item in tuple types #25717

Closed
4 tasks done
AlCalzone opened this issue Jul 17, 2018 · 4 comments
Closed
4 tasks done

Allow rest elements to not be the last item in tuple types #25717

AlCalzone opened this issue Jul 17, 2018 · 4 comments
Labels
Duplicate An existing issue was already created

Comments

@AlCalzone
Copy link
Contributor

AlCalzone commented Jul 17, 2018

Search Terms

rest element type tuple last

Related:

#24897

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)
@mhegazy
Copy link
Contributor

mhegazy commented Jul 17, 2018

Duplicate of #1360

@mhegazy mhegazy marked this as a duplicate of #1360 Jul 17, 2018
@mhegazy mhegazy added the Duplicate An existing issue was already created label Jul 17, 2018
@AlCalzone
Copy link
Contributor Author

@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
Copy link
Contributor

mhegazy commented 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
Copy link
Collaborator

Automatically closing this issue for housekeeping purposes. The issue labels indicate that it is unactionable at the moment or has already been addressed.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Duplicate An existing issue was already created
Projects
None yet
Development

No branches or pull requests

3 participants