GV

GeezerVison

The World Wide Web, restored to 800 pixels of glory.

Home|About|Gaming cabinet|Archives|How it works|Starter sites

On this site

  • Sign in
  • ActionsCI/CD and automation
  • CodespacesCloud development environments
  • All Features
  • Documentation
  • Training
  • Certifications
  • Build with Copilot
  • Customize Copilot
  • Integrations
  • Trending repos
  • GitHub Sponsors
  • Developer articles
  • Enterprises

This view

  • Original page
  • New address

GeezerVison is showing github.com — Add a way to prefer narrowing array to tuple if possible · Issue #27179 · microsoft/TypeScript

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

New issue
Copy link
New issue
Copy link
Closed
Closed
Add a way to prefer narrowing array to tuple if possible#27179
Copy link
Labels
QuestionAn issue which isn't directly actionable in codeAn issue which isn't directly actionable in code

Description

@Jamesernator
James Browning (Jamesernator)
opened on Sep 18, 2018
Issue body actions

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.

Activity

  1. dhruvrajvanshi commented on Sep 18, 2018

    @dhruvrajvanshi
    Dhruv Rajvanshi (dhruvrajvanshi)
    on Sep 18, 2018
    Contributor
    More actions

    You can force an array to be treated as a tuple by creating a function

    function tuple<Args extends any[]>(...args: Args): Args { return Args; }
    
    const pair = tuple(1, "string");
    // pair is inferred as [number, string]

    The rest of it seems a bit more tricky. Mapped types don't work properly with tuples so it might need language level support to convert one tuples type to another (sort of like array.map but for type level tuples)

    An even more general approach might be to allow recursion in types so we can use conditional types to write our own mapping logic.

    EDIT:

    Apparently, your Promise.race example doesn't need any language changes.

    interface PromiseConstructor {
        race<Ts extends Array<any>>(
            promises: Ts
        ): UnwrappedArray<Ts>
    }

    This works fine.

    The first example works too using the tuple function.

    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 = tuple(strs, nums);
    const z = zipLongest(zipPair)

    Basically, instead of doing [...Ts] you just write Ts.

    Reacted by Luke Miles and Va Da
  2. ahejlsberg commented on Sep 18, 2018

    @ahejlsberg
    Anders Hejlsberg (ahejlsberg)
    on Sep 18, 2018
    Member
    More actions

    I think you can already do this with features currently in typescript@next. In particular, the work in #26063 enables your scenario.

    type Iterablized<T> = { [K in keyof T]: Iterable<T[K]> };
    
    declare function zipLongest<T extends any[]>(iterables: Iterablized<T>): Iterable<T>;
    
    declare let triple: [Iterable<string>, Iterable<number>, Iterable<string[]>];
    
    let zippedTriple = zipLongest(triple);  // Iterable<[string, number, string[]>
  3. ahejlsberg commented on Sep 18, 2018

    @ahejlsberg
    Anders Hejlsberg (ahejlsberg)
    on Sep 18, 2018
    Member
    More actions

    With respect to explicit syntax for creating tuples from array literals, we're tracking and debating that in #16656.

    Meanwhile, with #24897 (in TS 3.0) you can easily add the following convenience function:

    function tuple<T extends any[]>(...elements: T) {
        return elements;
    }

    and you then have a very low cost general way of creating tuples. For example:

    const t1 = tuple(1, "hello");      // [number, string]
    const t2 = tuple(1, tuple(2, 3));  // [number, [number, number]]
    const t3 = tuple(1, [2, 3]);       // [number, number[]]
    const t4 = tuple();                // []
    Reacted by Bnaya Peretz, Paul Grau, Renato Ramos G., Ryan P Smith, Probhonjon Baruah, baofuchun and alissawix
  4. Jamesernator commented on Sep 18, 2018

    @Jamesernator
    James Browning (Jamesernator)
    on Sep 18, 2018
    Author
    More actions

    I've been trying it with typescript@next and the mapped types isn't quite the problem, strictly speaking the definition above works, it's just not particularly useful when doing it inline with inferred types e.g.:

    for (const [a, b] of zipLongest([strings, numbers]) {
      // Inferred type of both a and b are string | number
    }

    Which is what I'm proposing improving. Basically someway to say that if [strings, numbers] can be inferred as a tuple, it should be.

    This already works (in typescript@next) when the argument is variadic rather than as an array directly e.g.:

    type Iterablized<T> = { [K in keyof T]: Iterable<T[K] | undefined> };
    
    declare function zipLongest<Ts extends Array<any>>(...iterables: Ts): Iterablized<Ts>
    
    const strs = ['foo', 'bar', 'baz']
    const nums = [1, 2, 3]
    // Correctly inferred to be Iterable<[string | undefined, number | undefined]>
    const z = zipLongest(strs, nums)

    However this can't be used as I'm extending the zipLongest with additional arguments that can't be disambiguated from iterables. So it'll look something like this soon:

    const fillers = [() => '', () => 0]
    for (const [a, b] of zipLongest([strs, nums], fillers)) {
      
    }
  5. ahejlsberg commented on Sep 19, 2018

    @ahejlsberg
    Anders Hejlsberg (ahejlsberg)
    on Sep 19, 2018
    Member
    More actions

    James Browning (@Jamesernator) One trick that does work is to ensure that the contextual type for the array literal includes a tuple type. Once that is the case we'll infer tuple types:

    declare function zipLongest<Ts extends Iterable<any>[] | [Iterable<any>]>(iterables: Ts): Iterable<ZipUnwrapped<Ts>>;
    Reacted by Aleksey Levenstein, Tomás Fox, Salvatore and Andreas Bergmaier
  6. Jamesernator commented on Sep 19, 2018

    @Jamesernator
    James Browning (Jamesernator)
    on Sep 19, 2018
    Author
    More actions

    Oh that's useful, that should probably be added into the handbook somewhere as I don't think I would've ever figured that out.

    Reacted by John, Dan Levy and chocolateboy
  7. ahejlsberg commented on Sep 19, 2018

    @ahejlsberg
    Anders Hejlsberg (ahejlsberg)
    on Sep 19, 2018
    Member
    More actions

    Daniel Rosenwasser (@DanielRosenwasser) Wesley Wigham (@weswigham) FYI since we were discussing this the other day. Note that if we put this in the handbook, we should recommend using T extends XXX[] | [XXX] as the constraint pattern (versus T extends XXX[] | []) because this pattern ensures that members on T are just what you would see on XXX[]. (With an empty tuple in the pattern some never types creep in.)

    Reacted by Titian Cernicova-Dragomir, Tomás Fox, Aleksey Bobyr, Herrington Darkholme and EVA (Entity Value Attribute)
  8. Ryan Cavanaugh (RyanCavanaugh)
    added
    QuestionAn issue which isn't directly actionable in codeAn issue which isn't directly actionable in code
    on Sep 21, 2018
  9. TypeScript Bot (typescript-bot)
    closed this as completedon Jul 13, 2019
  10. typescript-bot commented on Jul 13, 2019

    @typescript-bot
    TypeScript Bot (typescript-bot)
    on Jul 13, 2019
    Contributor
    More actions

    This issue has been marked as 'Question' and has seen no recent activity. It has been automatically closed for house-keeping purposes. If you're still waiting on a response, questions are usually better suited to stackoverflow.

  11. Derek Lam (acrylic-origami)
    added a commit that references this issue on Sep 4, 2020

    Port to Typescript: first typecheck milestone

    a401727
  12. microsoft
    locked as resolved and limited conversation to collaborators on Oct 21, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    QuestionAn issue which isn't directly actionable in codeAn 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

      You are visitor 01402503 — GeezerVison does not store the pages you look up.
      Best viewed with Netscape Navigator 4.7 or Internet Explorer 6 at 800×600. No cookies required.