microsoft / TypeScript

TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
https://www.typescriptlang.org
Apache License 2.0
101.07k stars 12.49k forks source link

Boolean literal types and return type propagation for generators #2983

Closed JsonFreeman closed 5 years ago

JsonFreeman commented 9 years ago

This suggestion has a few pieces:

  1. Implement boolean literal types for true and false, in a fashion similar to #1003
  2. Implement type guards for booleans. Essentially, control flow constructs like if-else, while, for, etc would be subject to a type guard if their guard expression is a boolean.
  3. Now we can update generators to have a next method that returns { done: false; value: TYield; } | { done: true; value: TReturn; }, where TYield is inferred from the yield expressions of a generator, and TReturn is inferred from the return expressions of a generator.
  4. Iterators would return { done: false; value: TYield; } | { done: true; value: any; } to be compatible with generators.
  5. for-of, spread, array destructuring, and the type yielded by yield* would only pick the value associated with done being false.
  6. The value of a yield* expression would pick the value associated with done being true.
  7. Introduce a Generator type that can be used to track the desired type of a yield expression. This would be TNext, and would be the type of the parameter for the next method on a generator.

The generator type would look something like this:

interface Generator<TYield, TReturn, TNext> extends IterableIterator<TYield> {
    next(value?: TNext): IteratorYieldResult<TYield> | IteratorReturnResult<TReturn>;
    throw(exception: any): IteratorYieldResult<TYield> | IteratorReturnResult<TReturn>;
    return(value: any): IteratorYieldResult<TYield> | IteratorReturnResult<TReturn>;
    [Symbol.iterator](): Generator<TYield, TReturn, TNext>;
    [Symbol.toStringTag]: string;
}
falsandtru commented 6 years ago

One of the good usages of yield is communications with another process.

assert(5 === await new Coroutine<number, number, number>(async function* () {
    assert(1 === (yield 0));
    assert(3 === (yield 2));
    return 4;
}, { size: Infinity })[Coroutine.port].connect(function* () {
    assert(2 === (yield 1));
    assert(4 === (yield 3));
    return 5;
}));
hallettj commented 6 years ago

For the async task runner / coroutine / redux-saga use cases I also think it would be very helpful to be able to describe the type for TNext in terms of the specific type of the previously-yielded value. Basically what @Alxandr said, but I think it would be useful to express a mapping using a function type: the argument position provides spot to bind a variable for the type of the last yielded value. For example adapting @treybrisbane's example of a generator that yields promises:

function* c(): Generator<Promise<any>, string, <T>(yielded: Promise<T>) => T> {
  const aNumber = yield asyncAddTwo(4); // asyncAddTwo is of type `(n: number) => Promise<number>`
  const aString = yield asyncToString(aNumber * 2); // asyncToString is of type `(n: number) => Promise<string>`

  return `Result was ${aString}`;
}

Instead of providing a type for TNext this formulation provides the mapping <T>(yielded: Promise<T>) => T. The mapping might alternatively be an intersection of function types to specify different types for the next value depending on the yielded value.

I suggest that if the type of the last yielded value does not match the input type for the mapping then the type for the next value should implicitly be never.

(I apologize if I am repeating a suggestion that has already been made. I tried to scan the discussion so far, but I might have overlooked something.)

zpdDG4gta8XKpMCd commented 5 years ago

incorrect typing around return type just bit me, wow this issues is old

brainkim commented 5 years ago

If this is gonna be worked on, I think a nice easy win separate from typing the return value of generators would be to make the return and throw methods non-optional. I’m writing a lot of code like:

// having this function return an iterator type with non-optional `return` and
// `throw` results in a type error
function* gen(): IterableIterator<number> {
  yield 1;
  yield 2;
  yield 3;
}

// need an exclamation mark because return is optional, despite the fact
// that the return method is always defined for generator objects.
gen().return!();
oguimbal commented 5 years ago

I'm curious: Why adding generic parameters to Iterable and AsyncIterable, but not to IterableIterator and AsyncIterableIterator ?

... i have been waiting for this feature for a long time, but not extending iterableIterators forbids using strongly typed yield* result.

Works using this hack, though:

image

ilbrando commented 4 years ago

@s-panferov unfortunately not. You are talking about the async-runner style of generator used by things like co right?

The generator proposal (#2873) doesn't offer much typing support for async-runner generators. In particular:

  • All yield expressions are typed as any, as you can see in the example comments below. This is actually a very complex problem and I tried tackling it with some ideas which are all there in #2873. This won't change with #9407. And the TNext type in the OP above won't solve this either, since in an async runner there is generally not a single TNext type, but rather the type of each yield expression is a function of the type of that yield's operand (eg Promise<T> maps to T, Array<Promise<T>> maps to T[], etc). In particular the type of each yield expression is generally unrelated to the types of the other yield expressions in the body in an async-runner generator function.
  • The compiler infers a single TYield type that must be the best common type among all the yield operands. For async runners there often is no such best common type, so such generators often won't compile until at least one yield operand is cast to any. E.g. the function *bar example below doesn't compile for this reason.
  • Return types of generators are currently not tracked because without boolean literals, they can't be separated from the TYield type. This is what will be solvable once #9407 lands.

Example code:

interface User {id; name; address}
interface Order {id; date; items; supplierId}
interface Supplier {id; name; phone}
declare function getUser(id: number): Promise<User>;
declare function getOrders(user: User): Promise<Order[]>;
declare function getSupplier(id: number): Promise<Supplier>;

function* foo() {
    let user = yield getUser(42); // user is of type 'any'
    let user2 = <User> user;
    return user2; // This return type is not preserved
}

function* bar() { // ERROR: No best common type exists among yield expressions
    let user = yield getUser(42);       // user has type 'any'
    let orders = yield getOrders(user); // orders has type 'any'

    let orders2 = <Order[]> orders;
    let suppliers = yield orders2.map(o => getSupplier(o.supplierId)); // suppliers has type 'any'

    let suppliers2 = <Supplier[]> suppliers;
    return suppliers2; // This return type is not preserved
}

As it is not an easy task to make TypeScript able to handle generator functions like they are used by Redux Saga, I have come up with a workaround that works for me. You can see it here: https://github.com/ilbrando/redux-saga-typescript

danielnixon commented 4 years ago

@ilbrando another alternative: https://github.com/agiledigital/typed-redux-saga

ilbrando commented 4 years ago

Thank you @danielnixon this is a really clever solution.