microsoft / microsoft/TypeScript

Incorrect generic type inference of generator function

Open
#57,625 1 comment 2 reactions 1 assignee View on GitHub

@rbuckton is already working on this.

Since Mar 4, 2024.

Needs Investigation
Dominant language
Go
Stars
111k
Forks
14.4k
Avg merge
1d 19h
Merged PRs (30d)
117

Description

🔎 Search Terms
  • generator
  • yield*
🕗 Version & Regression Information

This is the behavior in every version I tried, and I reviewed the FAQ for entries about generators.

⏯ Playground Link

https://www.typescriptlang.org/play?target=99&jsx=0#code/JYOwLgpgTgZghgYwgAgMoHsC2EA86CuYyAggHzIDeAUMsgG5wA2+EAXCQNxUC+VVYATwAOKAPJCwwdCBxlkAXjRZccgD7J8IACYQYoCFq5UE0gM5EYULAHEIIaHDDooC5LIA0yAEKkAFDWQAcwgwW3soR2d2XwBKBXIwhycoHHFJaVlSTy9PMncqGPY0qRkfeMoAkxBzILsk51dg0LqI5NijWkYQ5CgIU3xGIkVg8MioADp7AA8wdr5aAHcAC2Au5F8AQl7+wfGtaQg46lpaKpr0CRLXbYGwcYZmCC4T5GAYdYv0kAV5RU0dPT2LRxXpgfBQEDPE43QaNFpjSYQGa+T4le5MFgxKG8AKg8HfCj0DFsHp9W7ox7Ibhcal8GCaBBfABUyCciVazkyKMu0iKPJkZEKyHZY1S-MyuVy5GOyDORAeLFcAmAEEYWmQqOkULxEKJjxpfFAkFgiBQqHwACNTAgoMB+RVaEJGHAQOxzLaQIEoXYtOwACKOJ48Q3gaDwJDIACqpmgDuQIDg2DdYA9XoCEEwcFWydTUP6Vptdq+fK+OHNBdt-NIBqoOgQzt6srMRCaABl0IFgloAJIgaPQaJxeTkYoZftQat8OW1MDtzsGXvj8vWytfVyxEslMuWldFkrkYcBSw2eFtekgRklFmxOO0AD0d5eT+QAD03++3wF74-iFBAvhsHADV3kEERkAAchvYdhVPTkMGwbcKz3aRyHUeDcHHVCNG0XR9C0Txl0LflPHHZAADI0B3IivlIcCv2QB9XlMeN0CIOBTFMYBAgTC01icZAhDgCJsGNYDWWEFBIKHBJYJSUcZEI1d9wIqilOkFSkKrcDxno6d8BjFxFGVVUtBZNlZN8NsOy7RcDNiLFdObZB813e0jJVNUzPQEU2n06Bxhc6iSgcl4dW+QK1MhAJuAc4wnKs+cez7AzFOQkAACZ1yFeTENcmj4iPKxMB8zlSLQ1S0o0vL918c9L2ka8jnoxjnxOD92o6j9msfAAVCTZSWCAEAAa2YrRFX4sBBuQC1gAYW0XQjdAQOmgADaYwFW5ARKWdB1QtQhXiIUxdoGLQQHAsBupmoa4D85BVrKyjNK+LbgGYkBWOQdjOO4uBeJQfjHoM8jnuq6RVp0l49JB9yTK8kqoEskI5xs5LoHsqFTiciK0qVDzTNZbyLL8iZcf5ELoRCfFnIq-lsQcoA

💻 Code
interface Some<out A> {
  value: A;
}

type Option<A> = Some<A> | undefined;

const fromGenerator = <A, B>(
  getGenerator: () => Generator<Option<A>, B, A>,
): Option<B> => {
  const generator = getGenerator();

  let result = generator.next();

  while (!result.done) {
    const option = result.value;
    if (option === undefined) return;
    result = generator.next(option.value);
  }

  return { value: result.value };
};

function* toGenerator<A>(option: Option<A>): Generator<Option<A>, A, A> {
  const value = yield option;
  return value;
}

interface Subscription {
  plan: string;
  end: Date;
}

interface User {
  name: string;
  email: string;
  subscription: Option<Subscription>;
}

declare const getLoggedInUser: () => Option<User>;

const getLoggedInUserSubscription = (): Option<Subscription> =>
  fromGenerator(function* () {
    //          ^^^^^^^^
    // Argument of type '() => Generator<Some<Subscription> | Some<User> | undefined, Subscription, User & Subscription>'
    // is not assignable to parameter of type '() => Generator<Option<Subscription>, Subscription, Subscription>'.
    const user = yield* toGenerator(getLoggedInUser());
    const subscription = yield* toGenerator(user.subscription);
    return subscription;
  });

const getLoggedInUserSubscription2 = (): Option<Subscription> =>
  fromGenerator<User | Subscription, Subscription>(function* () {
    //          ^^^^^^^^^^^^^^^^^^^
    // Type checks due to the bivariance of the `next` method but it shouldn't
    // because `User | Subscription` is not assignable to `User & Subscription`.
    const user = yield* toGenerator(getLoggedInUser());
    const subscription = yield* toGenerator(user.subscription);
    return subscription;
  });
🙁 Actual behavior

The generator function provided to fromGenerator has the type () => Generator<Some<Subscription> | Some<User> | undefined, Subscription, User & Subscription>.

TypeScript is not able to infer the generic type A of function fromGenerator in function getLoggedInUserSubscription. However, if I explicitly provide the generic types, fromGenerator<User | Subscription, Subscription>, like in getLoggedInUserSubscription2, then it type checks. But, it should be noted that it only type checks due to the bivariance of the next method.

next(...args: [] | [TNext]): IteratorResult<T, TReturn>;

Without bivariance, next(args_0: User & Subscription) => IteratorResult<Option<User | Subscription>, Subscription> is not assignable to next(args_0: User | Subscription) => IteratorResult<Option<User | Subscription>, Subscription> because User | Subscription is not assignable to User & Subscription.

🙂 Expected behavior

Bivariance aside, it would be brilliant if TypeScript was able to infer that A = User | Subscription and B = Subscription without the need for explicit type annotations. Expecting the user of fromGenerator to provide explicit type annotations is a deal-breaker for me. So, the only other solution is to use unknown instead of generics.

const fromGenerator = <B>(
  getGenerator: () => Generator<Option<unknown>, B, unknown>,
): Option<B> => {
  const generator = getGenerator();

  let result = generator.next();

  while (!result.done) {
    const option = result.value;
    if (option === undefined) return;
    result = generator.next(option.value);
  }

  return { value: result.value };
};

However, this is not ideal because

  1. It's less type safe. I can apply generator.next to any value. The type system doesn't restrict me from only applying generator.next to values of type A.
  2. The generic type A serves as a form of documentation which tells us that TYield and TNext are related by the equation TYield = Option<TNext>. When we replace the generic type A with unknown then this relation is lost.
Additional information about the issue

No response

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.