machty / ember-concurrency

ember-concurrency is an Ember Addon that enables you to write concise, worry-free, cancelable, restartable, asynchronous tasks.
http://ember-concurrency.com
MIT License
691 stars 155 forks source link

Use same types for return and arg #501

Open Techn1x opened 1 year ago

Techn1x commented 1 year ago

I'm trying to build a typed task that returns the same type that it was performed with

For example;

const abc: number = await someTask.perform(1)
const def: string = await someTask.perform('str')

I've come up with this as the task definition;

someTask = task(async <T>(result: T): Promise<T> => {
  return Promise.resolve(result)
})

But I can't seem to get the return type from the perform call to be generic, it seems stuck as unknown. Is there a way to do what I am trying to acheive?

It's probably got something to do with writing a Task type like this, but I can't quite figure out how to apply it to the task definition..

type SomeTask<T> = Task<T, [T]>
ombr commented 1 year ago

Hi @Techn1x,

I have the same issue on my side, I could create a sample reproduction:

import Service from '@ember/service';
import { task, timeout } from 'ember-concurrency';

export default class Test extends Service {
  createTask = task(
    { enqueue: true, maxConcurrency: 2 },
    async <H, K extends keyof H>(hash: H, key: K): Promise<H[K]> => {
      await timeout(100);
      return hash[key];
    }
  );

  async withoutTask<H, K extends keyof H>(hash: H, key: K): Promise<H[K]> {
    await timeout(100);
    return hash[key];
  }

  async test() {
    const a = await this.withoutTask({ a: 'string', b: 12 }, 'a');
    console.log(a);
    const res = await this.createTask.perform({ a: 'string', b: 12 }, 'a');
    console.log(res);
  }
}

// DO NOT DELETE: this is how TypeScript knows how to look up your services.
declare module '@ember/service' {
  interface Registry {
    test: Test;
  }
}

Let me know If I can help here.

ombr commented 1 year ago

Hi @Techn1x,

I think this is a limit of typescript... I did a lot of trial and error on the typescript playground, and here is a workaround that could work (forcing the type manually on the task).

const f = async <H, K extends keyof H>(h: H, k: K): Promise<H[K]> => {
  return h[k];
}
const res = f({a: 'string', b: 12}, 'b');

const task = <H,K extends keyof H>(f: (...args: [H, K])=> Promise<H[K]>): {process: (h: H, k: K)=> Promise<H[K]> } => {
  return {
    process: (h: H, k: K): Promise<H[K]>=> {
      return f(h, k);
    }
  }
}

type HackedTask = { process: <H,K extends keyof H>(h: H, k: K)=> Promise<H[K]> };

const t = (task(f) as unknown) as HackedTask;

const r = t.process({ a: 'string', b: true}, 'b');