Typescript: Use any instead of unknown for AsyncGenerator optional .next parameter

Created on 17 Sep 2019  路  1Comment  路  Source: microsoft/TypeScript

TypeScript Version: 3.6.2


Search Terms:
async generator, AsyncGenerator, optional .next

Code

The following causes a type error:

async function* sequence(iterable: AsyncIterable<number>): AsyncGenerator<number> {
  yield 12;
  try {
    // Type error: Cannot delegate iteration to value because the 'next' method of its iterator expects type 'undefined', but the containing generator will always send 'unknown'.
    yield* iterable;
  } finally {
    console.log('Cleanup!');
  }
}

Expected behavior:

I would've expected it to be a non-type error.

Problem:

The primary annoyance with this is that I'd like to use AsyncGenerator<T> just for specifying that .return() can be used without non-null assertions (.return!()) for early cleanup but AsyncGenerator<T> results in AsyncGenerator<T, any, unknown> so AsyncIterable<T> can't be delegated to.

Proposed solution:

Change interface AsyncGenerator<T = unknown, TReturn = any, TNext = unknown> to interface AsyncGenerator<T = unknown, TReturn = any, TNext = any.

This won't break anything as any is assignable to anything, and I doubt it'll be problematic as those using AsyncGenerator<T> (or AsyncGenerator<T, S>) presumably do not care about the .next parameter.

Needs Investigation

Most helpful comment

The problem with any as a default is that it allows you to violate the TNext constraint of the thing you are delegating to with yield*:

async function * g(): AsyncGenerator<number, void, string> {
  const x = yield;
  x.toUpperCase(); // because `x` *must* be a string
}

async function * f(other: AsyncGenerator<number, void, string>): AsyncGenerator<number> {
  yield* other; // no error because `any` is assignable to `string`
}

// compile time: no error because `1` is assignable to `any`
// runtime: errors because `toUpperCase` is not defined on a number
f(g()).next(1); 

Perhaps the default for TNext in AsyncIterator and Iterator should be unknown instead of undefined. I will need to run some tests to verify that this won't have other side effects.

>All comments

The problem with any as a default is that it allows you to violate the TNext constraint of the thing you are delegating to with yield*:

async function * g(): AsyncGenerator<number, void, string> {
  const x = yield;
  x.toUpperCase(); // because `x` *must* be a string
}

async function * f(other: AsyncGenerator<number, void, string>): AsyncGenerator<number> {
  yield* other; // no error because `any` is assignable to `string`
}

// compile time: no error because `1` is assignable to `any`
// runtime: errors because `toUpperCase` is not defined on a number
f(g()).next(1); 

Perhaps the default for TNext in AsyncIterator and Iterator should be unknown instead of undefined. I will need to run some tests to verify that this won't have other side effects.

Was this page helpful?
0 / 5 - 0 ratings