microsoft / microsoft/TypeScript
Incorrect generic type inference of generator function
@rbuckton is already working on this.
Since Mar 4, 2024.
- 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
💻 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
- It's less type safe. I can apply
generator.nextto any value. The type system doesn't restrict me from only applyinggenerator.nextto values of typeA. - The generic type
Aserves as a form of documentation which tells us thatTYieldandTNextare related by the equationTYield = Option<TNext>. When we replace the generic typeAwithunknownthen this relation is lost.
Additional information about the issue
No response
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Assessment
This issue has not been assessed yet.