Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Improper Type Inference in Async Generator Callback #57903

Closed
DScheglov opened this issue Mar 22, 2024 · 1 comment · Fixed by #58621
Closed

Improper Type Inference in Async Generator Callback #57903

DScheglov opened this issue Mar 22, 2024 · 1 comment · Fixed by #58621
Assignees
Labels
Bug A bug in TypeScript Fix Available A PR has been opened for this issue

Comments

@DScheglov
Copy link

DScheglov commented Mar 22, 2024

🔎 Search Terms

async generator inference, async generator

🕗 Version & Regression Information

  • This is a crash
  • This changed between versions 4.0.5 and 5.4.3
  • This changed in commit or PR _______
  • This is the behavior in every version I tried, and I reviewed the FAQ for entries about _________
  • I was unable to test this on prior versions because _______

⏯ Playground Link

https://www.typescriptlang.org/play?target=99&jsx=0&ts=5.4.3#code/JYOwLgpgTgZghgYwgAgEoQM4FcA2YA8AKgDTICiAfMgN4BQyyA2gMoCeAtgEYD2OAdMEhQ4YblAC6ACgCUALmQBxCCGgix+MqUIVaAX1q0wrAA4oActzDpseAPIwiVALzJCyCAA9IIACYY0mLgEcCCspCGsVAD8yCoAbtDI8oQA3IYmKGRQUIQZ9o7ILm6e3n4BNgRYIADWINwA7iCkoDCJlMgxZEmxEAlQaUamyLbVuab52oWu7l7KZdZB+C2JqKRVtQ0g0Wjd8dADGcgAghisIAgAUtycRKTtLjKFVCdnCEoqwqJQGloUabQ+CAIHBwKAoBDcEAYMDIOCnc4AEW48lu5AokgAVtd5C9zlcbiQ0dInsgAApQbjsYAYCD4BZ4fD0YajPIOSYAH2QFisgTsbIoxCZXU5WRyrMctAofwBQJBYOQEKhMPYcGMopRmmQADF0SrjKYoPJJNAKYbyMSnFQtRaqI5JGCKvJ6QRCZQbeVFoSdf9BigAELcbjVKbUZDAHzyaFQUAAcxSyDAghwEEjYGjIDjsKwYAAFmIAJIR5BR2PIXQHIZHbN5qAhsNFksZ+MgODsFPFtOl8vpIYBoMAdUEOarubEUz7wYAZDQs6OzSOa2X-oDgaDwZDochWmAEDmJ0bw6n0zH3eTKdTac78BPSAAiMy2QgAfS1tgAqmYEbepcvZWuFRuMLbruC5iAeDadhmp4UlSNJ0ryBCgVAd4Ps+r4fl+P4GJ4xhiDCiqbsBe6BtUg65khACMhRMpIPBBoWR6xjaTIMHCrxIpIbHnFuVQIImkIAFTII8dAMGJAFKsgdHVPIE5TKwwAQDgPhCXA9RwIIW4QDuxFBrRJGFtILHicgfC5sokh6qKkiPJayD3o+L7vp+T5+rYtgANK3rC-gEWA0hGcZDB+bONY4tWY4uApSkqbC6maURSH6UGfBwBFUCGUFYlmTmFlWdkNnug5aHOQiT5HG+hAABK2Kg3lwhJ0IBQYJnIGCYBYFAIAznwvXSeE6Vlj5yATmRw7pWk4m6NI-w4XhjVAdpu6jUOSEAEzUQwyXVAxHbHnIZIwRe8EVNeJFjUhKGOehLluZ53mcsVTkYeVlU1XVUpPMZXEIBxP08ec-EgEJIlZSF0mySR8mKcpqnxYtOkTttmWtQwOV5aq1m2VQT03WVd1ecNfnNWDgGhWBxyDVFMOxWpGkIyB6Xbal6Uo6j6MgJZmMFdj9moc9LkVdVtX1b5gEk617Wdd1oa9Xw-Xk7WujDSt5ETcZ01pEAA

💻 Code

interface Result<T, E> {
  [Symbol.iterator](): Generator<E, T>
}

type NotResultOf<T> = T extends Result<any, any> ? never : T;
type ErrTypeOf<T> = T extends Result<unknown, infer E> ? E : never;
type OkTypeOf<T> = T extends Result<infer R, unknown> ? R : never;
type AsyncJob<T, E> = () => AsyncGenerator<E, T>;

declare const asyncDo: <T, E>(job: AsyncJob<T, E>) => Promise<Result<
  OkTypeOf<T> | NotResultOf<T>,
  E | ErrTypeOf<T>
>>;
declare const mapErr: <E, F>(mapper: (error: E) => F) => <T>(result: Result<T, E>) => Result<T, F>;

type Book = { id: string; title: string; authorId: string };
type Author = { id: string; name: string };
type BookWithAuthor = Book & { author: Author };

declare const fetchBook: (id: string) => Promise<Result<Book, "NOT_FOUND">>;

declare const fetchAuthor: (id: string) => Promise<Result<Author, "NOT_FOUND">>;

export const fetchBookWithAuthor1 =
  (bookId: string) =>
    asyncDo(async function* () {
      const book: Book = yield* await fetchBook(bookId)
        .then(mapErr(() => "NOT_FOUND_BOOK" as const))

      const author: Author = yield* await fetchAuthor(book.authorId)
        .then(mapErr(() => "NOT_FOUND_AUTHOR" as const))

      return { ...book, author } as BookWithAuthor;
    });

export const fetchBookWithAuthor2 =
  (bookId: string): Promise<Result<BookWithAuthor, "NOT_FOUND_BOOK" | "NOT_FOUND_AUTHOR">> =>
    asyncDo(async function* () {
      const book: Book = yield* await fetchBook(bookId)
        .then(mapErr(() => "NOT_FOUND_BOOK" as const))

      const author: Author = yield* await fetchAuthor(book.authorId)
        .then(mapErr(() => "NOT_FOUND_AUTHOR" as const))

      return { ...book, author } as BookWithAuthor;
    });

🙁 Actual behavior

Compilation Error in the function fetchBookWithAuthor2

Type 'Author | BookWithAuthor' is not assignable to type 'Author'.
Property 'name' is missing in type 'BookWithAuthor' but required in type 'Author'.(2322)
input.ts(17, 29): 'name' is declared here.

🙂 Expected behavior

No errors.
compilation result must be the same as for fetchBookWithAuthor1.

The only difference between two versions of the function is a return type specified.

The defenition file is emitted correctly:

interface Result<T, E> {
    [Symbol.iterator](): Generator<E, T>;
}
type Book = {
    id: string;
    title: string;
    authorId: string;
};
type Author = {
    id: string;
    name: string;
};
type BookWithAuthor = Book & {
    author: Author;
};
export declare const fetchBookWithAuthor1: (bookId: string) => Promise<Result<BookWithAuthor, "NOT_FOUND_BOOK" | "NOT_FOUND_AUTHOR">>;
export declare const fetchBookWithAuthor2: (bookId: string) => Promise<Result<BookWithAuthor, "NOT_FOUND_BOOK" | "NOT_FOUND_AUTHOR">>;
export {};

Additional information about the issue

No response

@RyanCavanaugh
Copy link
Member

Slightly reduced (could go farther probably) + annotated

interface Result<T, E> {
  [Symbol.iterator](): Generator<E, T>
}

type NotResultOf<T> = T extends Result<any, any> ? never : T;
type ErrTypeOf<T> = T extends Result<unknown, infer E> ? E : never;
type OkTypeOf<T> = T extends Result<infer R, unknown> ? R : never;
type AsyncJob<T, E> = () => AsyncGenerator<E, T>;

declare const asyncDo: <T, E>(job: AsyncJob<T, E>) => Promise<Result<
  OkTypeOf<T> | NotResultOf<T>,
  E | ErrTypeOf<T>
>>;
declare const mapErr: <E, F>(mapper: (error: E) => F) => <T>(result: Result<T, E>) => Result<T, F>;

type Book = { id: string; title: string; authorId: string };
type Author = { id: string; name: string };
type BookWithAuthor = Book & { author: Author };

declare const fetchAuthor: (id: string) => Promise<Result<Author, "NOT_FOUND">>;

let authorId: string = "";

const f1 = asyncDo(async function* () {
  const author: Author = yield* await fetchAuthor(authorId)

  return null! as BookWithAuthor;
});

const f2: Promise<Result<BookWithAuthor, "NOT_FOUND_BOOK" | "NOT_FOUND_AUTHOR">> = asyncDo(async function* () {
  
  // Without yield*, the type of test1 is
  //   Result<Author, "NOT_FOUND_AUTHOR>
  const test1 = await fetchAuthor(authorId).then(mapErr(() => "NOT_FOUND_AUTHOR" as const))
  //     ^?

  // With yield*, the type of test2 is
  //    Author | BookWithAuthor
  // But this codepath has no way to produce BookWithAuthor
  const test2 = yield* await fetchAuthor(authorId).then(mapErr(() => "NOT_FOUND_AUTHOR" as const))
  //    ^?

  return null! as BookWithAuthor;
});

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Bug A bug in TypeScript Fix Available A PR has been opened for this issue
Projects
None yet
Development

Successfully merging a pull request may close this issue.

4 participants