See this GitHub issue for more info:
microsoft/TypeScript#41646 - Allow generic yield*
types
By using a constrained generic type parameter, the return type of your generator function can be derived from its arguments.
type Chain = <Iters extends readonly Iterable<unknown>[]>(
...iters: Iters
) => Iters[number];
In the function signature above, the generic Iters
must be assignable to a type that is a readonly array of Iterable<unknown>
elements. So, each argument in the rest parameter iters
must be assignable to Iterable<unknown>
. This will allow the compiler to infer the yielded type of each iterable argument.
Here's an example of applying it to the implementation that you showed, and then using it with a couple of example functions from your playground link in order to see the inferred return type:
declare function test1(): Iterable<number>;
declare function test2(): Iterable<string>;
const chain: Chain = function* (...iters) {
for (let it of iters) yield* it;
};
const iter = chain(test1(), test2());
//^? const iter: Iterable<number> | Iterable<string>
for (const value of iter) {}
//^? const value: string | number
You can see that the inferred return type is Iterable<number> | Iterable<string>
, and that using it in a for...of
loop produces a yielded value that is the union of the yield type of each iterable in the union.
I think this already produces a satisfactory result according to your question criteria, but the actual return type can still be improved to more accurately represent the returned iterable itself.
By using a type utility (adapted from content in the linked GitHub issue) which extracts the yielded type from within an iterable type:
type YieldedFromIterable<
I extends
| Iterable<unknown>
| Iterator<unknown>
| IterableIterator<unknown>
| Generator<unknown>
> = I extends
| Iterable<infer T>
| Iterator<infer T>
| IterableIterator<infer T>
| Generator<infer T>
? T
: never;
...an alternative return type can be created for the function:
type Chain = <Iters extends readonly Iterable<unknown>[]>(
...iters: Iters
) => Iterable<YieldedFromIterable<Iters[number]>>;
In juxtaposition with the first function signature's return type (which was a union of each of the iterable arguments), this one is a single iterable which yields a union value derived from each argument — and using it looks like this:
const chain: Chain = function* (...iters) {
for (let it of iters) yield* it as any;
};
const iter = chain(test1(), test2());
//^? const iter: Iterable<string | number>
for (const value of iter) {}
//^? const value: string | number
Code in TS Playground
Concluding thoughts:
Not using a type assertion on the yielded value of the second example causes a compiler error:
const chain: Chain = function* (...iters) { /*
~~~~~
Type '<Iters extends readonly Iterable<unknown>[]>(...iters: Iters) => Generator<unknown, void, undefined>' is not assignable to type 'Chain'.
Call signature return types 'Generator<unknown, void, undefined>' and 'Iterable<YieldedFromIterable<Iters[number]>>' are incompatible.
The types returned by '[Symbol.iterator]().next(...)' are incompatible between these types.
Type 'IteratorResult<unknown, void>' is not assignable to type 'IteratorResult<YieldedFromIterable<Iters[number]>, any>'.
Type 'IteratorYieldResult<unknown>' is not assignable to type 'IteratorResult<YieldedFromIterable<Iters[number]>, any>'.
Type 'IteratorYieldResult<unknown>' is not assignable to type 'IteratorYieldResult<YieldedFromIterable<Iters[number]>>'.
Type 'unknown' is not assignable to type 'YieldedFromIterable<Iters[number]>'.(2322) */
for (let it of iters) yield* it;
};
I think this is caused by a limitation of TypeScript's control flow analysis (but perhaps I'm wrong about that and someone else can provide more clarity).
A solution is to assert the value's type, like this:
const chain: Chain = function* (...iters) {
for (let it of iters) yield* it as Iterable<YieldedFromIterable<typeof iters[number]>>; // ok
};
// OR
const chain: Chain = function* (...iters) {
for (let it of iters) yield* it as any; // ok
};