microsoft / microsoft/TypeScript

Incorrect generic type inference of generator function

Offen
#57,625 1 Kommentar 2 Reaktionen 1 zugewiesene Person Auf GitHub ansehen

@rbuckton arbeitet bereits daran.

Seit 04.3.2024.

Needs Investigation
Vorherrschende Sprache
Go
Sterne
111k
Forks
14.4k
Ø Merge
1 T. 19 Std.
Gemergte PRs (30 T.)
117

Beschreibung

🔎 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

Beitragsleitfaden

Beitragsleitfaden öffnen

Erste Schritte

  1. Lies das ganze Issue und danach den Beitragsleitfaden des Projekts.
  2. Schreib ins Issue, dass du es übernimmst — das erspart doppelte Arbeit.
  3. Forke das Repository und arbeite in einem Branch.
  4. Öffne einen Pull Request, der die Issue-Nummer nennt.

Bewertung

Dieses Issue wurde noch nicht bewertet.

Neue Issues direkt in Ihr Postfach

Eine kurze Übersicht über anfängerfreundliche GitHub-Issues.