skip to Main Content

I have a LazyPromise object with all method. It works like Promise.all but the promises start the call not on initialization, eg.:

// It started executing as soon as it was declared
const p1 = Promise.all([Promise.resolve(1), Promise.resolve('foo')])

// It started executing only when `p2.then()` is called
const p2 = LazyPromise.all([() => Promise.resolve(1), () => Promise.resolve('foo')])

this is the implementation

class LazyPromise {
  static all(lazies: Array<() => Promise<any>>){
    return Promise.all(lazies.map(lazy => lazy()));
  }
}

usage

const result = LazyPromise.all([() => Promise.resolve(1), () => Promise.resolve('foo')]).then(result => console.log(result))

I want make the result with correct types.
Now result is a any[] I want: [number, string]

How infer the correct result types?

2

Answers


  1. Chosen as BEST ANSWER

    Respond my self after a bit of deep diving into Promise.all typescript definition. The final trick is this:

    class LazyPromise {
        
        all<T>(values: [() => Promise<T>]): Promise<T[]>;
        all<T1, T2>(values: [() => Promise<T1>, () => Promise<T2>]): Promise<[T1, T2]>;
        all<T1, T2, T3>(values: [() => Promise<T1>, () => Promise<T2>, () => Promise<T3>]): Promise<[T1, T2, T3]>;
        all<T1, T2, T3, T4>(values: [() => Promise<T1>, () => Promise<T2>, () => Promise<T3>, () => Promise<T4>]): Promise<[T1, T2, T3, T4]>;
        all<T1, T2, T3, T4, T5>(values: [() => Promise<T1>, () => Promise<T2>, () => Promise<T3>, () => Promise<T4>, () => Promise<T5>]): Promise<[T1, T2, T3, T4, T5]>;
        async all<T>(values: ReadonlyArray<() => Promise<T>>): Promise<ReadonlyArray<Awaited<T>>> {
            return Promise.all(values.map((value) => value()));
        }
    }
    

    not beautiful, but works

    const result = LazyPromise.all([
      () => Promise.resolve(1), 
      () => Promise.resolve('foo')
    ]) // result is [number, string]
    

  2. This is just a little bit tricky. You want a generic, but because of the way function types are inferred the most obvious way to do it fails:

    class LazyPromise {
      static all<T>(lazies: Array<() => Promise<T>>){
        return Promise.all(lazies.map(lazy => lazy()));
      }
    }
    
    const result = LazyPromise.all([
      () => Promise.resolve(1), 
      () => Promise.resolve('foo') // error, expects a number
    ]).then(result => console.log(result))
    

    The compiler can’t or won’t infer the union of the return values of the thunks for T. You can work around this by declaring a function constraint instead and extracting the return value’s type:

    // Little utility helper type
    type ExtractPromiseType<T> = T extends Promise<infer R> ? R : never
    
    class LazyPromise {
      static all<F extends () => any>(
        lazies: Array<F>
      ): Promise<Array<ExtractPromiseType<ReturnType<F>>>> {
        return Promise.all(lazies.map(lazy => lazy()));
      }
    }
    
    const result = LazyPromise.all([() => Promise.resolve(1), () => Promise.resolve('foo')])
    async function foo() {
      const res = await result
      const first = res[0]  // number | string
      const second = res[1] // number | string
    }
    

    Note that this does not treat the array as a tuple but rather the way Typescript normally treats heterogeneous arrays, i.e. as a union of the possible types. The function signature is a bit more complex, but it provides enough hints to the compiler to get you what you want.

    Playground

    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search