3

I've got the following function:

function* chain(...iters) {
    for (let it of iters)
        yield* it
}

It accepts a list of iterables and creates a generator that yields sequentially from each one.

I'm not sure how to type it correctly to support mixed-type iterables. If my inputs are like Iterable<X>, Iterable<Y> etc then the result should be Iterable<X | Y>. How to write this for a variadic argument?

Playground Link

gog
  • 10,367
  • 2
  • 24
  • 38
  • This looks like a use-case of [existential types](https://en.wikipedia.org/wiki/Type_system#Existential_types). – vighnesh153 Feb 06 '23 at 09:56
  • 2
    This answer might be helpful to your question: https://stackoverflow.com/a/67842566/3977134 - using that, this could work for you: `chain(...iters: T): Iterable` – r3dst0rm Feb 06 '23 at 10:02
  • 1
    Does something like [this](https://tsplay.dev/weBZ1N) meet your needs? If so, I can turn it into an answer. If not, what did I miss? – jsejcksn Feb 06 '23 at 10:51
  • @jsejcksn: I don't really understand that, so an answer with an explanation would be appreciated. – gog Feb 06 '23 at 11:48
  • [^](https://stackoverflow.com/questions/75359560/typing-the-generator-chain-function/75361766#comment132975295_75359560) @gog Ok, I posted one. – jsejcksn Feb 06 '23 at 13:12

1 Answers1

4

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
};
jsejcksn
  • 27,667
  • 4
  • 38
  • 62
  • `type YieldedFromIterable> = I extends Iterable ? T : never;` is sufficient. `yield*` like `for..of` can not deal with `Iterator` if it ain't an `IterableIterator`. And both `IterableIterator` and `Generator` inherit from `Iterable`. – Thomas Feb 06 '23 at 13:32
  • [^](https://stackoverflow.com/questions/75359560/typing-the-generator-chain-function/75361766#comment132977085_75361766) @Thomas Thanks for commenting that observation — I agree and considered exactly that while writing it. I decided to leave others in the union for future readers who might find this question while trying to solve a slightly different problem (and might not think about modifying it otherwise). – jsejcksn Feb 06 '23 at 13:36