Skip to content

Provide a way to type a fix-length tuple-like generator/iterator #42033

Description

@zojize

Search Terms

  • generator
  • iterator

Suggestion

Express a fixed-length generator.

Use Cases

  • Allows the spread syntax on functions to know how many arguments is deconstructed (see examples below)

Examples

stackoverflow question

// Vector2.ts
class Vector2 {
  constructor (public x, public y) {
    this.x = x;
    this.y = y;
  }

  //... A bunch of vector methods

  public* [Symbol.iterator]: Generator<number, void, unknown> {
    yield this.x;
    yield this.y;
  }
}

// main.js
const canvas = document.querySelector('#canvas');
const ctx = canvas.getContext('2d');

const a = new Vector2(0, 0);
const b = new Vector2(10, 10);

ctx.beginPath();
ctx.moveTo(...a);
// works correctly, but with this warning: 
// Expected 2 arguments, but got 0 or more.ts(2556)
ctx.lineTo(...b);
ctx.stroke();
ctx.closePath();

Right now, the code above produces the correct behavior. However, typescript doesn't not recognize how many arguments is passed to ctx.moveTo and ctx.lineTo therefore it warns me about it.

My Implementation

There could be an tuple-like syntax for generators as well, for example:

public* [Symbol.iterator]: *[number, number] {
    yield this.x;
    yield this.y;
  }

With this syntax, typescript recognizes how many argument is deconstructed by the spread operator, and knows its a generator with the prepending *.

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. library functionality, non-ECMAScript syntax with JavaScript output, etc.)
  • This feature would agree with the rest of TypeScript's Design Goals.

Activity

  1. treybrisbane commented on Dec 30, 2020

    @treybrisbane

    Wow, I just came here to raise this, only to find this issue created only a couple weeks ago! Crazy timing! 😮

    In any case, the use-cases presented in #32523 also require (or rather, are partially enabled by) this feature. Specifically, those use-cases are:

  2. treybrisbane commented on Dec 30, 2020

    @treybrisbane

    Thinking a bit about type inference...

    I imagine it would be straightforward for simple cases, right?

    For example, the inferred return type of

    function* foo() {
      yield 0;
      yield 1;
    }

    would presumably be either *[0, 1] or *[number, number] (to borrow SIGUSR97's syntax).

    As soon as loops are involved though, it looks like things get much more fun! 😅

    For example, what would the inferred return type of

    function* foo() {
      for (let n = 0; n < 5; n += 1) {
        yield n;
      }
    }

    be?
    I'm guessing something like *number[]?

    Let's get more complex though. What would the inferred return type of

    function* foo() {
      yield 0;
    
      for (let n = 1; n < 4; n += 1) {
        yield n;
      }
    
      yield 4;
    }

    be?
    Maybe something like *[0, ...*number[], 4] or *[number, ...*number[], number]?

    When you start thinking about nested loops, things look a bit scary. Generics and conditionals further terrify me. 😱

  3. treybrisbane commented on Dec 30, 2020

    @treybrisbane

    Ron Buckton (@rbuckton) You seem to be Generator Man; got any thoughts on this? 😜

  4. aleclarson commented on Jul 8, 2023

    @aleclarson

    Can we get an official comment explaining why this isn't higher priority? I want to destructure a class instance as if it was a tuple. I define 0 and 1 getters on the prototype and both have different type signatures.

  5. thehappycheese commented on Jun 22, 2024

    @thehappycheese

    Arrived here with the exact same use-case and issue as Jeff Zou (@zojize); vector types and the canvas.

    Feels like I should be able to manually specify something like TupleGenerator<[number, number], ...> It could de-sugar to something like below;

    class Vector2 {
      x:number;
      y:number;
      // public* [Symbol.iterator]: TupleGenerator<[number, number], void, unknown> { // <--desired syntax
      //   yield this.x;
      //   yield this.y;
      // }
      public* [Symbol.iterator]: Generator<number, void, unknown> { // de-sugared
        yield this.x;
        yield this.y;
      }
      public toTuple():[number, number]{ // de-sugared
        return [this.x, this.y]
      }
    }
    
    let a = new Vector2(1,2);
    // ctx.moveTo(...a);  // <-- desired syntax
    ctx.moveTo(...a.toTuple()); // de-sugared

    Static type inference for *[Symbol.iterator] is obviously impossible for complex iterators... but why not have this for manually specified types :)

  6. zojize commented on Aug 14, 2024

    @zojize
    Author

    thehappycheese I submitted a PR based on your suggestion, let me know what you think 😃

  7. FrameMuse commented on Sep 29, 2024

    @FrameMuse

    Have the same need, my case is very specific and it some might not like this, but I believe this is very handy.

    Consider any state in modern frameworks, they usually like [get, set] = state or { get, set } = state. What I want is to provide isomorphic structure that supports both but only on demand. I don't want to create an array when it's not relevant and I don't want developers to see array methods exposed as well as they are not relevant.

    class State<T> {
      constructor(private value: T) { }
    
      get(): T { return this.value }
      set(value: T): void { this.value = value }
    
      *[Symbol.iterator](): TupleGenerator<[() => T, (value: T) => void]> { // <= Expectation
        yield () => this.get()
        yield (value: T) => this.set(value)
      }
    }

    Currently, when I try to destruct as array, the types are not correct.

  8. iliaamiri commented on Oct 5, 2024

    @iliaamiri

    My use-case is that I want to create a minimal array that only has the tuple values and no other methods such as: map, flatMap, forEach, etc.

    export type MinimalArray<T, E> = {
      0: E
      1: T
      length: 2
      [Symbol.iterator](): Iterator<E | T> // I can't pass the tuple values here. It will make them a union type. 
    }

    Let's say I created an object and artificially implemented the [Symbol.iterator]() method myself.

    class CustomTuple<T1, T2> {
      private readonly _values: [T1, T2]; // Internal array-like storage for elements
    
      constructor(first: T1, second: T2) {
        this._values = [first, second];
    
        // Define properties 0 and 1 to enable indexed access (like a tuple)
        Object.defineProperty(this, '0', {
          value: first,
          enumerable: false,
          writable: false,
        });
    
        Object.defineProperty(this, '1', {
          value: second,
          enumerable: false,
          writable: false,
        });
      }
    
      // Length property to match tuple behavior
      length: 2 = 2;
    
      // Iterator for spreading and iteration
      [Symbol.iterator](): Iterator<T1 | T2> {
        let index = 0;
        return {
          next: (): IteratorResult<T1 | T2> => {
            if (index < this._values.length) {
              return { value: this._values[index++], done: false };
            } else {
              return { value: undefined, done: true };
            }
          },
        };
      }
    }

    When I destructure it, I get a union type:

    const tuple = new CustomTuple(1, "bar")
    
    const [a, b] = tuple
    // a -> string | number
    // b -> string | number

    The nice thing about my CustomTuple is that, I don't see the extra Array API that comes with a regular Array object:
    image

    So the user cannot do tuple[0] or tuple[1] either. That's what I want.


    So, there is no way I can achieve the same result as defining a standard fixed tuple:

    const foo = [1, "bar"]
    
    const [a, b] = foo
    // a -> number
    // b -> string
  9. runspired commented on Aug 18, 2025

    @runspired

    It was mentioned to me that folks didn't think polymorphic/megamorphic generators are commonly in use and perhaps this is why TS has not focused on improving the types here, but they are. Multiple popular libraries use only megamorphic signatures while encouraging either compilation or use of any to paper over the limitations of using generators.

    The transpiler route: these libraries compile async/await in the bodies of specially marked functions into generator functions with yield in order to enable proper typing.

    The nothing-to-see-here route: these libraries have the same limitation but won't tell you that in their docs while showing you nice examples. They presume you're ok with a lot of any and never in your types or will just write JS and not care about the types.

    This said, I suspect the underlying JS spec for generators and iterators makes it exceedingly hard/probably impossible to create a linkedlist or well-known-series structure that works both for typing the function* body and for typing the produced generator/iterator since TS can't shorten the list each time next is called.

  10. FrameMuse commented on Aug 18, 2025

    @FrameMuse

    Chris Thoburn (@runspired) Yep, it seems like that

    type TupleGenerator<T extends [...unknown[]]> = {
      next(): IteratorYieldResult<T>
      // ^ Can't be dynamically typed by subsequent calls since TS types don't have state.
    }
    
    class State<T> {
      constructor(private value: T) { }
    
      get(): T { return this.value }
      set(value: T): void { this.value = value }
    
      *[Symbol.iterator](): TupleGenerator<[() => T, (value: T) => void]> { // <= Expectation
        yield () => this.get()
        yield (value: T) => this.set(value)
      }
    }
    const [get, set] = new State(1)

    I'm not into how TS is implemented, but if it can track deep nested types and do some operations that require states, probably this could be implemented as well.

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

    Awaiting More FeedbackThis means we'd like to hear from more people who would be helped by this featureSuggestionAn idea for TypeScript

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions