Skip to content

Add a way to prefer narrowing array to tuple if possible #27179

Closed

Description

Search Terms

tuple narrow, prefer tuple

Suggestion

This is a meta-suggestion over the top of some of the cases in #16896 and #26113, basically at current it's quite repetitive to specify overloads for tuple types and we tend to wind up things like this:

// etc up to how ever many overloads might be needed
function zipLongest<T1, T2, T3, T4>(
  iterables: [Iterable<T1>, Iterable<T2>, Iterable<T3>, Iterable<T4>]
): Iterable<[T1, T2, T3, T4]>

function zipLongest([]): []
function zipLongest<T>(iterables: Array<Iterable<T>>): Iterable<Array<T>>

We can already specify the type if we know only tuples are going to be input e.g.:

type Unwrap<T> = T extends Iterable<infer R> ? R : never
type ZipUnwrapped<T> = { [P in keyof T]: Unwrap<T[P]> }

declare function zipLongest<Ts extends Array<Iterable<any>>>(iterables: Ts): Iterable<ZipUnwrapped<Ts>>

const strs = ['foo', 'bar', 'baz', 'boz']
const nums = [1, 2, 3, 4]
const zipPair: [string[], number[]] = [strs, nums]
const z = zipLongest(zipPair)

In this case the type of z is correctly inferred to be Iterable<[string, number]>, but if we remove the type declaration on zipPair then we get that the inferred type of z is Iterable<(number | string)[]>.

Basically what I propose is adding some way to specify that an array should be narrowed to the most precise tuple possible if we can.

There's a couple approaches that might be used:

Approach 1

Use the [...TupleParam] spread syntax already mentioned in #26113 and when this syntax is seen narrow to the best tuple we could get. So the above declaration would become:

// The only difference is iterables: Ts has become iterables: [...Ts]
declare function zipLongest<Ts extends Array<Iterable<any>>>(iterables: Ts): Iterable<ZipUnwrapped<Ts>>

This has the downside that it might for any Ts in [Some, Tuple, Params, ...Ts] to be narrowed even when the wider type was what was intended.

Approach 2

Add some new syntax that specifies that if a certain value can be narrowed into a tuple then we should do so:

// Arbitrary syntax 1
declare function zipLongest(
  iterables: [...tuple Ts]
): Iterable<ZipUnwrapped<Ts>>

// Arbitrary syntax 2
declare function zipLongest<Ts extends [!...Iterable<any>]>(
  iterables: Ts
): Iterable<ZipUnwrapped<Ts>>

The syntax isn't really all that important. The main downside of this approach is that we need two overloads, one for the narrowed tuple and one for the non-narrowed tuple. Additionally extra syntax would need to be supported and maintained.

Examples

Other than the above example another example that would benefit is the builtin lib.promise.d.ts which specifies overloads in this repetitive overloading pattern.

We could write something like Promise.all like so:

type UnwrapMaybePromiseLike<T>
= T extends PromiseLike<infer R> ? R : T

type UnwrappedArray<T extends Array<any>> = {
    [P in keyof T]: UnwrapMaybePromiseLike<T[P]>
}

interface PromiseConstructor {
    all<Ts extends Array<any>>(
        promises: [...Ts]
    ): UnwrappedArray<Ts>
}

And inference would just work.

// a would inferred to be [number, number]
const a = Promise.all([1, 2])

// b would be inferred to be [string, number]
const b = Promise.all(['foo', Promise.resolve(12)])

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)

EDIT: Realized Promise.race was supposed to be Promise.all oops.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Assignees

No one assigned

    Labels

    QuestionAn issue which isn't directly actionable in code

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions