microsoft / microsoft/TypeScript

Incorrect generic type inference of generator function

Aperta
#57,625 1 commento 2 reazioni 1 assegnatario Vedi su GitHub

@rbuckton ci sta già lavorando.

Dal 4/3/2024.

Needs Investigation
Lingua principale
Go
Stelle
111k
Fork
14.4k
Merge medio
1g 19h
PR unite (30g)
117

Descrizione

🔎 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

Guida per i contributori

Apri la guida per i contributori

Come iniziare

  1. Leggi tutta la issue e poi la guida ai contributi del progetto.
  2. Commenta sulla issue per dire che te ne occupi tu — evita che due persone facciano lo stesso lavoro.
  3. Fai un fork del repository e lavora su un branch.
  4. Apri una pull request che faccia riferimento al numero della issue.

Valutazione

Questa issue non è ancora stata valutata.

Ricevi le nuove issue nella tua casella

Un breve riepilogo di issue GitHub adatte ai principianti.