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

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

Open
5 tasks done
zojize opened this issue Dec 18, 2020 · 8 comments
Open
5 tasks done

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

zojize opened this issue Dec 18, 2020 · 8 comments
Labels
Awaiting More Feedback This means we'd like to hear from more people who would be helped by this feature Suggestion An idea for TypeScript

Comments

@zojize
Copy link

zojize commented Dec 18, 2020

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.
@RyanCavanaugh RyanCavanaugh added Awaiting More Feedback This means we'd like to hear from more people who would be helped by this feature Suggestion An idea for TypeScript labels Dec 23, 2020
@treybrisbane
Copy link

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:

@treybrisbane
Copy link

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. 😱

@treybrisbane
Copy link

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

@aleclarson
Copy link

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.

@thehappycheese
Copy link

Arrived here with the exact same use-case and issue as @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 :)

@zojize
Copy link
Author

zojize commented Aug 14, 2024

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

@FrameMuse
Copy link

FrameMuse commented Sep 29, 2024

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.

@iliaamiri
Copy link

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Awaiting More Feedback This means we'd like to hear from more people who would be helped by this feature Suggestion An idea for TypeScript
Projects
None yet
Development

No branches or pull requests

7 participants