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

Trouble implementing .then #33416

Closed
maxnordlund opened this issue Sep 13, 2019 · 1 comment
Closed

Trouble implementing .then #33416

maxnordlund opened this issue Sep 13, 2019 · 1 comment
Assignees
Labels
Rescheduled This issue was previously scheduled to an earlier milestone Working as Intended The behavior described is the intended behavior; this is not a bug

Comments

@maxnordlund
Copy link

[email protected]

I added to libraries: $ tsc --lib es2018,dom --target es2018 test-case.ts.

Search Terms:
AsyncIterator, AsyncGenerator, Then, Promise, PromiseLike

Code

/**
 * Promise.prototype.then onfufilled callback type.
 */
type Fulfilled<R, T> = ((result: R) => T | PromiseLike<T>) | null;

/**
 * Promise.then onrejected callback type.
 */
type Rejected<E> = ((reason: any) => E | PromiseLike<E>) | null;

class Waiter<T, TReturn, TNext> implements PromiseLike<T> {
  source: AsyncGenerator<T, TReturn, TNext | undefined>;

  constructor(
      source: () => AsyncGenerator<T, TReturn, TNext | undefined>,
  ) {
    this.source = source();
  }

  then(onfufilled?: Fulfilled<TReturn, T>, onrejected?: Rejected<T>): Promise<T> {
    return this.implementation().then(onfufilled, onrejected);
  }

  private async implementation(): Promise<TReturn> {
    // Drain the async iterator by hand since for-await-of would
    // discard the returned value.
    while (true) {
      const { done, value } = await this.source.next();

      // We know value will be a TReturn as the async iterator
      // has already been exhausted
      if (done) return value as TReturn
    }
  }
}

async function main() {
  let waiter = new Waiter(async function* gen() {
    yield 123;
    return "abc";
  });

  try {
    let result = await waiter;
    console.log(
      `expect ${JSON.stringify(result)} to equal "abc"`,
      result === "abc"
     );
  } catch (error) {
    console.error("Oh no", error)
  }

  let other = await waiter.then(() => 123);
  console.log(`expect ${JSON.stringify(other)} to equal 123`, other === 123);
}

main().catch(console.error)

Expected behavior:
The code to compile, and correctly infer the return type for other in the example above.

Running it with node gives the correct output:

expect "abc" to equal "abc" true
expect 123 to equal 123 true

Actual behavior:

test-case.ts(20,3): error TS2416: Property 'then' in type 'Waiter<T, TReturn, TNext>' is not assignable to the same property in base type 'PromiseLike<T>'.
  Type '<S, E>(onfufilled?: (result: TReturn) => S | PromiseLike<S>, onrejected?: (reason: any) => E | PromiseLike<E>) => Promise<S | E>' is not assignable to type '<TResult1 = T, TResult2 = never>(onfulfilled?: (value: T) => TResult1 | PromiseLike<TResult1>, onrejected?: (reason: any) => TResult2 | PromiseLike<TResult2>) => PromiseLike<TResult1 | TResult2>'.
    Types of parameters 'onfufilled' and 'onfulfilled' are incompatible.
      Types of parameters 'value' and 'result' are incompatible.
        Type 'TReturn' is not assignable to type 'T'.
          'TReturn' is assignable to the constraint of type 'T', but 'T' could be instantiated with a different subtype of constraint '{}'.

When poking around in VS Code it correctly infers that await waiter is a string, which is nice. But await waiter.then(() => 123) becomes unknown instead of number. I'm guessing it's because of the type error above, but still pretty amazing that it can infer anything at all.

I also tried inlining the definitions of Fulfilled and Rejected, but no dice. I also tried changing all type parameters to T, but again nothing:

test-case.ts(20,3): error TS2416: Property 'then' in type 'Waiter<T, TReturn, TNext>' is not assignable to the same property in base type 'PromiseLike<T>'.
  Type '(onfufilled?: (result: TReturn) => T | PromiseLike<T>, onrejected?: (reason: any) => T | PromiseLike<T>) => Promise<T>' is not assignable to type '<TResult1 = T, TResult2 = never>(onfulfilled?: (value: T) => TResult1 | PromiseLike<TResult1>, onrejected?: (reason: any) => TResult2 | PromiseLike<TResult2>) => PromiseLike<TResult1 | TResult2>'.
    Types of parameters 'onfufilled' and 'onfulfilled' are incompatible.
      Types of parameters 'value' and 'result' are incompatible.
        Type 'TReturn' is not assignable to type 'T'.
          'TReturn' is assignable to the constraint of type 'T', but 'T' could be instantiated with a different subtype of constraint '{}'.

Playground Link

Related Issues:
#16993
#15599
#33239
#31264

@RyanCavanaugh RyanCavanaugh added the Needs Investigation This issue needs a team member to investigate its status. label Sep 16, 2019
@RyanCavanaugh RyanCavanaugh added this to the TypeScript 3.7.0 milestone Sep 16, 2019
@RyanCavanaugh RyanCavanaugh added the Rescheduled This issue was previously scheduled to an earlier milestone label Aug 31, 2020
@rbuckton
Copy link
Member

rbuckton commented Dec 6, 2021

This doesn't seem to be a bug in TypeScript, but rather an issue with the definition of the type.

The T in Waiter<T, TReturn, TNext> isn't actually used in implementation(), and the type variable returned in then should be its own type variable rather than the T in Waiter. Here's a version with types that match the implementation:

/**
 * Promise.prototype.then onfufilled callback type.
 */
type Fulfilled<R, T> = ((result: R) => T | PromiseLike<T>) | null;

/**
 * Promise.then onrejected callback type.
 */
type Rejected<E> = ((reason: any) => E | PromiseLike<E>) | null;

class Waiter<T, TReturn, TNext> implements PromiseLike<TReturn> {
  source: AsyncGenerator<T, TReturn, TNext | undefined>;

  constructor(
      source: () => AsyncGenerator<T, TReturn, TNext | undefined>,
  ) {
    this.source = source();
  }

  then<TResult1, TResult2 = TResult1>(onfufilled?: Fulfilled<TReturn, TResult1>, onrejected?: Rejected<TResult2>): Promise<TResult1 | TResult2> {
    return this.implementation().then(onfufilled, onrejected);
  }

  private async implementation(): Promise<TReturn> {
    // Drain the async iterator by hand since for-await-of would
    // discard the returned value.
    while (true) {
      // NOTE(rbuckton): The `T` in `AsyncGenerator<T, ...>` is never 
      //                 sent: ---------------------v
      const { done, value } = await this.source.next();

      // We know value will be a TReturn as the async iterator
      // has already been exhausted
      if (done) return value as TReturn
    }
  }
}

async function main() {
  let waiter = new Waiter(async function* gen() {
    yield 123;
    return "abc";
  });

  try {
    let result = await waiter;
    console.log(
      `expect ${JSON.stringify(result)} to equal "abc"`,
      result === "abc"
     );
  } catch (error) {
    console.error("Oh no", error)
  }

  let other = await waiter.then(() => 123);
  console.log(`expect ${JSON.stringify(other)} to equal 123`, other === 123);
}

main().catch(console.error)

Playground link

@rbuckton rbuckton closed this as completed Dec 6, 2021
@rbuckton rbuckton added Working as Intended The behavior described is the intended behavior; this is not a bug and removed Needs Investigation This issue needs a team member to investigate its status. labels Dec 6, 2021
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Rescheduled This issue was previously scheduled to an earlier milestone Working as Intended The behavior described is the intended behavior; this is not a bug
Projects
None yet
Development

No branches or pull requests

4 participants