-
Notifications
You must be signed in to change notification settings - Fork 12.5k
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
Generator: infer the type of yield expression based on yielded value #32523
Comments
For reference, there's some discussion and ideas about this in #30790 (e.g. #30790 (comment)). |
This is not currently possible to model in the type system, as is mentioned in #30790, as it requires some mechanism to define a relationship between a TYield and a TNext to be preserved between separate invocations. |
Two additional use cases are:
|
You need Rank-2 types: https://prime.haskell.org/wiki/Rank2Types |
type TestAsyncGen = {
next<T>(value: T): IteratorResult<Promise<T>, void>
}
function* test(): TestAsyncGen {
const value = yield Promise.resolve(123)
} I just tried this, currently it errors on
Would it be possible to support this use case this way? For the record, this currently works and is properly typechecked: type TestNumberStringGen = {
next(value: number): IteratorResult<string, number>
}
function* test(): TestNumberStringGen {
const num = yield "123"
return num
} |
No, it is not possible. Your definition of function start(gen) {
const result = gen.next();
return result.done
? Promise.resolve(result.value)
: Promise.resolve(result.value).then(value => resume(gen, value));
}
function resume(gen, value) {
const result = gen.next(value);
return result.done
? Promise.resolve(result.value)
: Promise.resolve(result.value).then(value => resume(gen, value));
} The type system has no way to handle that today. What you would need is something like #32695 (comment), where calling a method can evolve the interface Start<R> {
next<T>(): IteratorResult<Promise<T>, Promise<R>> & asserts this is Resume<T, R>;
}
interface Resume<T, R> {
next<U>(value: T): IteratorResult<Promise<U>, Promise<R>> & asserts this is Resume<U, R>;
} If such a feature were to make it into the language, then you would be able to inform the |
Is it possible to go for a simpler alternative? Something like having an second, richer model for generator functions: Rough sketch: interface BaseYieldType {
(arg:any):any
}
interface Coroutine<
Args extends any[],
RetType,
BaseCoroutineProduction,
YieldType extends BaseYieldType
> {
(...args:Args):Generator<BaseCoroutineProduction, RetType, any>
__yield: YieldType; // replace with some TS only symbol maybe
} Libraries would be able to specify the argument type, e.g. type AwaitingYield = <T>(p:Promise<T>) => T;
declare function coroutine<Args extends any[], Ret>(
genfn: Coroutine<Args, Ret, Promise<unknown>, AwaitingYield>
) : (...args:Args) => Ret;
which would cause TS to infer the type of yield expressions here: // This type cast wont be necessary, instead the type will be
// inferred from the specified Coroutine<...> argument to coroutine()
let x = coroutine(function* f() {
let a = yield Promise.resolve(1);
let b = yield Promise.resolve(true);
return Promise.resolve('a')
}) as Coroutine<[], Promise<string>, Promise<any>, AwaitingYield>) |
I'm very interested in support for algebraic effects really just for one use-case: I have a database that I want to be able to run async with a backend or sync in memory. I believe coroutines / generators are a perfect way to do this. The types don't quite work out but I feel like its pretty close: export type API = {
get: {
request: { type: "get"; id: string }
response: { id: string; value: any }
}
set: {
request: { type: "set"; id: string; value: any }
response: void
}
}
function syncApi<T extends keyof API>(
request: API[T]["request"]
): API[T]["response"] {
return {} as any
}
async function asyncApi<T extends keyof API>(
request: API[T]["request"]
): Promise<API[T]["response"]> {
return {} as any
}
// Define a custom IterableIterator class.
interface APIGenerator {
next<T extends keyof API>(
response: API[T]["response"]
): IteratorResult<API[T]["request"], void>
[Symbol.iterator](): APIGenerator
}
// Some helpers
function get(id: string): API["get"]["request"] {
return { type: "get", id }
}
function set(id: string, value: any): API["set"]["request"] {
return { type: "set", id, value }
}
// Reusable logic that works for both sync and async code.
// It appears to typecheck, but the return value from yield doesn't resolve.
function* logic(): APIGenerator {
const x = yield get("1") // const x: API[T]["response"]
x.id // 💥
x.value // 💥
const y = yield set("2", 3) // const y: API[T]["response"]
}
// Here's how we can evaluate both sync and async
function runSync() {
const gen = logic()
let request = gen.next() // 🤔 what do we pass here?
while (request.value !== undefined) {
request = gen.next(syncApi(request.value))
}
}
async function runAsync() {
const gen = logic()
let request = gen.next() // 🤔 what do we pass here?
while (request.value !== undefined) {
request = gen.next(await asyncApi(request.value))
}
} Two problems:
I understand that there is an internal technical limitation for this to work, but I thought I'd add my use-case here to try to motivate why it would be useful! |
I came up with a simpler way to do it that involves a type-cast helper function. Seems like macros would be useful here. (no playground link because it hadn't upgraded to 3.6 yet) Code
// Algebraic Effects API.
//
// The goal here is to define a sync API and an async API for a database without having
// to duplicate logic.
//
// It seems to come with the overhead of duplicative syntax.
//
// The two sync API datase API functions
function get(args: { id: string }): { id: string; value: any } {
return {} as any
}
function set(args: { id: string; value: any }): {} {
return {} as any
}
const api = { get, set }
const apiNames = Object.keys(api) as Array<APIName>
type API = typeof api
type APIName = keyof API
type APIRequest<N extends APIName = APIName> = Parameters<API[N]>[0]
type APIResponse<N extends APIName = APIName> = ReturnType<API[N]>
// Generator yields Messages that the handler will process
type APIMessage<N extends APIName = APIName> = {
apiName: N
request: APIRequest<N>
}
// API handlers that service requests.
function syncApiHandler<N extends APIName>(
message: APIMessage<N>
): APIResponse<N> {
return {} as any
}
async function asyncApiHandler<N extends APIName>(
message: APIMessage<N>
): Promise<APIResponse<N>> {
return {} as any
}
/**
* Using Typescript 3.6 Generator type.
* - yields `Message`
* - returns `any`
* - next argument is `APIResponse`
*
* Note: we cannot define a type in Typescript that lets us infer:
*
* const result: APIResponse<T> = yielded (message as Message<T>)
*
* Thus, we use `n` and `m` helpers below.
*
*/
type LogicGenerator = Generator<APIMessage, any, APIResponse>
/**
* Helper function to narrow the response type from `yield`.
* Needs to be used with the same `keyof m`.
*/
declare const n: {
get(response: APIResponse): APIResponse<"get">
set(response: APIResponse): APIResponse<"set">
}
/**
* Helper to construct messages passed to `yield`.
* Needs to be used with the same `keyof n`.
*/
declare const m: {
get(request: APIRequest<"get">): APIMessage<"get">
set(request: APIRequest<"set">): APIMessage<"set">
}
// Reusable logic that works for both sync and async code.
function* logic(): Generator<APIMessage, any, APIResponse> {
// Plumbing is required to effectively `await` a sync or async interface using `yield`.
const setRequest: APIRequest<"set"> = { id: "hello", value: "world" }
const message: APIMessage<"set"> = m.set(setRequest)
// After yield does not infer APIResponse<"message">
const response: APIResponse = yield message
// This is a cast so it's important that `n.[apiName]` is the same as `m.[apiName]`.
const setResponse: APIResponse<"set"> = n.set(response)
// We can chain these together to make it easier to read.
const a = n.set(yield m.set({ id: "hello", value: "world" }))
const { id, value } = n.get(yield m.get({ id: "hello" }))
// prettier-ignore
// Using custom indentation helps group together all of the redundant `set` that is
// prone to human error.
const b = n.set(
yield m.set({ id: "hello", value: "world" }))
// Ideally we could define a type for Generator that `yield` would understand how to infer.
// Then we could get away with the code below.
const c = yield m.set({ id: "hello", value: "world" })
}
// Here's how we can evaluate both sync and async
function runSync() {
const gen = logic()
let request = gen.next()
while (request.value !== undefined) {
request = gen.next(syncApiHandler(request.value))
}
}
async function runAsync() {
const gen = logic()
let request = gen.next()
while (request.value !== undefined) {
request = gen.next(await asyncApiHandler(request.value))
}
} |
Some types are now typed 'unknown' (in particular from yield). See microsoft/TypeScript#32523
I just ran into this limitation again. @RyanCavanaugh what are your thoughts on implementing typed algebraic effects. Is this something that is realistically not going to happen anytime soon? As an avid TypeScript user, this issue as well has higher-kinded types are the only real roadblocks I've run into. But it sounds like it might be a fundamental limitation of the way TypeScript was designed... |
Another popular library that runs into this issue is mobx-state-tree. Asynchronous state updates are modeled with generators. This issue combined with #35105 causes a bunch of |
While having a strong typechecking on the executor part would be great, the biggest limitation for implementing coroutines at the moment is that On the executor side I would be happy to have a method While this would have messier code on the executor side, the "client" side for coroutine libraries would stay clean.
There are three issues, as a DSL developer, that I have with this:
|
Also note that the type of |
I've opened a proposal for a fix here: 💡 Yield Overrides #43632 It goes the other way around (the yielded expression includes the returned type, rather than the generator driving it), but I think it's a reasonable approach that mirrors the preexisting |
Here's something that's type-safe and compatible with current Typescript. // Increment / Decrement from:
// https://stackoverflow.com/questions/54243431/typescript-increment-number-type
// We intersect with `number` because various use-cases want `number` subtypes
type ArrayOfLength<N extends number, A extends any[] = []> =
A["length"] extends N ? A : ArrayOfLength<N, [...A, any]>;
type Inc<N extends number> = number & [...ArrayOfLength<N>, any]["length"]
type Dec<N extends number> = number & (ArrayOfLength<N> extends [...infer A, any] ? A["length"] : -1);
type RangeType<Start extends number, End extends number> =
number & (
Start extends End
? never
: Start | RangeType<Inc<Start>, End>);
// Generator definition
type Yield<YieldValue, NextValue> = (_: YieldValue) => NextValue
type AnyYield = Yield<never, unknown>
type YieldValue<Y extends AnyYield> = Y extends Yield<infer YV, unknown> ? YV : never;
type NextValue<Y extends AnyYield> = Y extends Yield<never, infer NV> ? NV : never;
interface SpecificGenerator<Ys extends AnyYield[], R>
extends Generator<YieldValue<Ys[number]>, R, NextValue<Ys[number]>> {
// `next<I extends 0>(): ...` is also valid,
// I just used an optional param because it gives better intellisense.
next<I extends 0>(value?: any): IteratorYieldResult<YieldValue<Ys[0]>>;
next<I extends RangeType<1, Ys["length"]>>(value: NextValue<Ys[Dec<I>]>):
IteratorYieldResult<YieldValue<Ys[I]>>;
next<I extends Ys["length"]>(value: NextValue<Ys[Dec<I>]>): IteratorReturnResult<R>;
next<I extends number>(value: NextValue<Ys[Dec<I>]>): IteratorResult<YieldValue<Ys[I]>, R>;
[Symbol.iterator](): this;
}
// Example 1
type ExampleGenerator = SpecificGenerator<[(_: number) => string, (_: number) => number], string>
const g = function* () {
const a = (yield 3) as string;
const b = (yield a.length) as number;
return a + b.toString();
} as () => ExampleGenerator;
const gRunner = function (egf: () => ExampleGenerator) {
const eg = egf();
const { value: n } = eg.next<0>();
const { value: m } = eg.next<1>("a".repeat(n));
const { value: result } = eg.next<2>(m);
return result;
}
// Example 2
// From https://curiosity-driven.org/monads-in-javascript#do
// Monad
type Monad<T> = {
bind<U>(f: (t: T) => Monad<U>): Monad<U>
}
type MonadInnerType<MT extends Monad<any>> = MT extends Monad<infer T> ? T : never;
// Maybe
interface Maybe<T> extends Monad<T> { };
class Just<T> implements Maybe<T> {
value: T;
constructor(value: T) {
this.value = value;
}
bind<U>(f: (value: T) => Monad<U>): Monad<U> {
return f(this.value);
}
toString() {
return `Just(${this.value})`;
}
}
const Nothing = {
bind<U>(_: (value: never) => Monad<U>): Monad<U> {
return this as Monad<U>;
},
toString() {
return "Nothing";
},
} as Maybe<never>;
type MonadUnwrappers<Vs extends any[]> = { [I in keyof Vs]: (_: Monad<Vs[I]>) => Vs[I] };
function doNotation<Vs extends any[], T>(genFunc: () => SpecificGenerator<MonadUnwrappers<Vs>, Monad<T>>) {
const gen = genFunc();
function step<I extends number>(value: Vs[Dec<I>]): Monad<T> {
const result = gen.next<I>(value);
if (result.done) {
return result.value;
}
type V = Vs[I];
return result.value.bind<T>((v: V) => step<Inc<I>>(v));
}
return step<0>(undefined);
}
const sampleDoBody = function* () {
var value = (yield new Just(5)) as number;
var value2 = (yield new Just(6)) as number;
return new Just(value + value2);
} as () => SpecificGenerator<[(_: Just<number>) => number, (_: Just<number>) => number], Monad<number>>;
const result = doNotation(sampleDoBody); |
@rbuckton wrote (#32523 (comment)):
Dang, and for a moment there I thought what we were missing was a way to parameterize Generator with the relationship between those two types, but you're right, those aren't even related to the same call. Is there a programming language that can handle that? |
When you define a generator function in JavaScript, the semantics of the We already have a way to type type Awaiter = <T>(value: T) => Awaited<T>;
function* example(yield: Awaiter) {
const value1 = yield Promise.resolve(1); // infer typeof value1 = number
const value2 = yield Promise.resolve('hello'); // infer typeof value2 = string
}
await co(example); You can still explicitly type the generator type or rely on type inference to determine its type. The Update: found out about #36967 which is proposing exactly this. |
Search Terms
generator, iterator, yield, type inference, redux-saga, coroutine, co, ember-concurrency
Suggestion
There are a number of JS patterns that require a generator to be able to infer the type of a
yield
statement based on the value that wasyield
ed from the generator in order to be type-safe.Since #2983 was something of a "catch-all" issue for generators, now that it has closed (🙌) there isn't a specific issue that tracks this potential improvement, as far as I can tell. (A number of previous issues on this topic were closed in favor of that issue, e.g. #26959, #10148)
Use Cases
Use Case 1 - coroutines
A coroutine is essentially
async/await
implemented via generators, rather than through special language syntax. [1] There exist a number of implementations, such as Bluebird.coroutine, the co library, and similar concepts such as ember-concurrencyIn all cases, it's pretty much syntactically identical to
async/await
, except usingfunction*
instead ofasync function
andyield
instead ofawait
:Currently, there really isn't a better approach than explicit type annotations on every yield statement, which is completely unverified by the type-checker:
The most correct we can be right now, with TS3.6 would be to express the generator type as
Generator<Promise<string> | Promise<User>, string, string | User>
- but even that would require every the result of everyyield
to be discriminated betweenstring
andUser
.It's clearly not possible to know what the type of a yield expression is just by looking at the generator function, but ideally the types for
coroutine
could express the relationship between the value yielded and the resulting expression type: which is something liketype ResumedValueType<Yielded> = Yielded extends Promise<infer T> ? T : Yielded
.Use Case 2 - redux-saga
redux-saga is a (fairly popular) middleware for handling asynchronous effects in a redux app. Sagas are written as generator functions, which can yield specific effect objects, and the resulting expression type (and the runtime behavior) depend on the value yielded.
For example, the
call
effect can be used analogously to the coroutine examples above: the generator will call the passed function, which may be asynchronous, and return the resulting value:The relationship between the
yield
ed value and the resulting value is more complex as there are a lot of possible effects that could be yielded, but the resulting type could still hypothetically be determined based on the value that was yielded.This above code is likely even a bit tricker than the coroutine as the saga generators aren't generally wrapped in a function that could hypothetically be used to infer the
yield
relationship: but if it were possible to solve thecoroutine
case, a wrapping function for the purposes of TS could likely be introduced:I imagine this would be a difficult issue to tackle, but it could open up a lot of really expressive patterns with full type-safety if it can be handled. In any case, thanks for all the hard work on making TS awesome!
[1] As an aside, to the tangential question of "why would you use a coroutine instead of just using
async/await
?". One common reason is cancellation - Bluebird promises can be cancelled, and the cancellation can propagate backwards up the promise chain, (allowing resources to be disposed or API requests to be aborted or for polling to stop, etc), which doesn't work if there's a nativeasync/await
layer.The text was updated successfully, but these errors were encountered: