Repository navigation
Provide a way to type a fix-length tuple-like generator/iterator #42033
Description
Activity
- addedAwaiting More FeedbackThis means we'd like to hear from more people who would be helped by this featureThis means we'd like to hear from more people who would be helped by this featureSuggestionAn idea for TypeScriptAn idea for TypeScript
on Dec 23, 2020 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:
- Coroutines
redux-saga's Sagasmobx-state-tree's Asynchronous Actions- Emulation of Haskell's
dosyntax for sequencing monadic actions (example) - Emulation of Algebraic Effects (explanation, and example)
Reacted by Harry Solovay and Aadit M ShahThinking 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. 😱
Reacted by Jeff ZouRon Buckton (@rbuckton) You seem to be Generator Man; got any thoughts on this? 😜
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
0and1getters on the prototype and both have different type signatures.Reacted by lionel-rowe, keatkeat87, graypegg, Jeff Zou, Arlen Beiler, andrea-cerlini and Alvaro Bernar SilosArrived 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 :)Reacted by Jeff Zou and Daryl Zunigathehappycheese I submitted a PR based on your suggestion, let me know what you think 😃
Reacted by thehappycheese, Valery Zinchenko and EiliyaHave 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] = stateor{ 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.
Reacted by Eiliya, Alvaro Bernar Silos, Gulg and James BrumondMy 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
CustomTupleis that, I don't see the extra Array API that comes with a regular Array object:

So the user cannot do
tuple[0]ortuple[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
Reacted by Valery Zinchenko, Jeff Zou, Sʜɪᴍᴜʀᴀ Yū, Alvaro Bernar Silos and GulgIt 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
anyto 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.
- ember-concurrency (recently ported to svelte as sheepdog)
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
anyandneverin your types or will just write JS and not care about the types.- https://effect.website/docs/getting-started/using-generators/
- https://frontside.com/effection/guides/v3/collections/
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.
Reacted by Valery ZinchenkoChris 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.
Search Terms
Suggestion
Express a fixed-length generator.
Use Cases
Examples
stackoverflow question
Right now, the code above produces the correct behavior. However, typescript doesn't not recognize how many arguments is passed to
ctx.moveToandctx.lineTotherefore it warns me about it.My Implementation
There could be an tuple-like syntax for generators as well, for example:
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: