cefn
cefn

Reputation: 3331

Inferring return type of generated async functions

I expect to be able to infer the return type of async functions when they are yielded as values from a generator.

In the example below, inference of the type of the yielded async functions seems to work, (they are detected as () => Promise<string> ). However, T is not able to be inferred as string and ends up unknown.

I have reduced a more complex problem to the example below (see playground). It retains all the essential features I think I need for the real case.

The commented code indicates a bit more about the reason for needing to infer both J and T. Crucially, extensions to Job<T> can carry job-specific metadata.

Can anyone work out how to compose type definitions that can infer both J and T in the below case, so that the settlement type is JobSettlement<string, () => Promise<string>> instead of JobSettlement<unknown, () => Promise<string>>

JobSettlement has T of unknown

I speculate this is one of the unhandled cases of microsoft/TypeScript#47599 but I am unsure how to put it into a shape where TypeScript can handle the inference.

type Job<T> = () => Promise<T>

export interface JobFulfilment<T, J extends Job<T>> {
  job: J;
  status: "fulfilled";
  value: T;
}

export interface JobRejection<T, J extends Job<T>> {
  job: J;
  status: "rejected";
  reason: unknown;
}

export type JobSettlement<T, J extends Job<T>> =
  | JobFulfilment<T, J>
  | JobRejection<T, J>;

export async function* createSettlementSequence<T, J extends Job<T>>(
  createJobSequence: () => AsyncGenerator<J>
): AsyncIterable<JobSettlement<T, J>> {
    for await (const job of createJobSequence()){
        try{
            const value = await job();
            yield {
                job,
                value,
                status:"fulfilled",
            }
        }
        catch(reason){
            yield {
                job,
                reason,
                status:"rejected"
            }
        }
    }
}

const settlementSequence = createSettlementSequence(async function* () {
    yield async () => "foo"
    yield async () => "bar"
    yield async () => "baz"
})


// const settlementSequenceWithMetadata = createSettlementSequence(async function* () {
//     yield Object.assign(async () => "foo", { task: 0})
//     yield Object.assign(async () => "bar", { task: 1})
//     yield Object.assign(async () => "baz", { task: 2})
// })

Upvotes: 0

Views: 181

Answers (1)

cefn
cefn

Reputation: 3331

A naive attempt to simplify this to have just one generic binding (J extends Job) and to infer T via a Fulfilment<J extends Job<unknown>> fails as per this playground

enter image description here

However, this can be addressed by explicitly asserting the type via...

(await job()) as Awaited<ReturnType<typeof job>>

It seems surprising that Typescript needs to be told this, but hey.

type Job<T> = () => Promise<T>;

export interface JobFulfilment<J extends Job<unknown>> {
  job: J;
  status: "fulfilled";
  value: Awaited<ReturnType<J>>;
}

export interface JobRejection<J extends Job<unknown>> {
  job: J;
  status: "rejected";
  reason: any;
}

export type JobSettlement<J extends Job<unknown>> =
  | JobFulfilment<J>
  | JobRejection<J>;

export async function* createSettlementSequence<J extends Job<unknown>>(
  createJobSequence: () => AsyncGenerator<J>,
): AsyncIterable<JobSettlement<J>> {
  for await (const job of createJobSequence()) {
    try {
      const value = (await job()) as Awaited<ReturnType<typeof job>>;
      yield {
        job,
        value,
        status: "fulfilled",
      };
    } catch (reason) {
      yield {
        job,
        reason,
        status: "rejected",
      };
    }
  }
}

const settlementSequence = createSettlementSequence(async function* () {
  yield Object.assign(async () => "foo", { task: 0 });
  yield Object.assign(async () => "bar", { task: 1 });
  yield Object.assign(async () => "baz", { task: 2 });
});

for await (const settlement of settlementSequence) {
  if (settlement.status === "fulfilled") {
    const { value, job } = settlement;
    console.log(`Task ${job.task} successfully returned ${value}`);
  }
}

Upvotes: 0

Related Questions