Search code examples
javascripttypescripttype-conversionmapped-types

TypeScript: Wrap function parameters in Promise<T>


I try to create a type that takes a function type, wraps the function parameters in Promise<> and returns a new type - the same function but with parameters as Promise, for example:

type PromisedFn = PromisedArgs<(a: number, b: string | number) => void>
// should produce (a: Promise<number>, b: Promise<string | number>) => void

After hours of racking my brains I only managed to achieve such code:

type Func = (id: number, guid: number | string) => number | string; 
type FuncParams = Parameters<Func>;
// FuncParams = [id: string, guid: string | number]

type FuncWrappedParams = {
  [K in keyof FuncParams as Extract<K, '0'|'1'>]: Promise<FuncParams[K]>
}
// { 0: Promise<number>, 1: Promise<string | number> }

Still number-indexed object can't be applied as an array properly:

type FuncWrappedParamsArray = [...args: FuncWrappedParams[]];
type WrappedParamsFunc = (...args: FuncWrappedParamsArray) => ReturnType<Func>;

let func: WrappedParamsFunc = (
  id: Promise<number>,
  guid: Promise<number | string>
) => 'TestString';

// ERROR:
// Type '(id: Promise<number>, guid: Promise<number | string>) => string'
// is not assignable to type 'WrappedParamsFunc'.
// Types of parameters 'id' and 'args' are incompatible.
// Type 'FuncWrappedParams' is missing the following properties from type 'Promise<number>':
// then, catch, [Symbol.toStringTag]

I have no idea how to handle this one.

Problem #1 is as above.

Problem #2: the disadvantage is that we must know a number of function parameters ahead of time: [K in keyof FuncParams as Extract<K, '0'|'1'>].


Solution

  • You can write PromisedArgs like this:

    type PromisedArgs<T extends (...args: any) => any> =
      T extends (...args: infer A) => infer R ? (
        (...args: { [I in keyof A]: Promise<A[I]> }) => R
      ) : never;
    

    As long as your array type is a generic type parameter (like A above), a mapped type over it will produce another array type. In {[I in keyof A]: ...}, the key type I is essentially only iterating over the numeric-like indices, automatically. You don't have to manually grab "0" | "1" | "2" | ... or worry about how to promote the resulting mapped type back into an array.

    The use of the infer keyword in the conditional type check is just a way to get both the parameters and return type of the function at once, instead of using the Parameters<T> and the ReturnType utility types separately. If you look at the definitions of Parameters and of ReturnType you'll see that they are implemented the same way.

    Anyway, let's make sure this works as intended:

    type Func = (id: number, guid: number | string) => number | string;
    type WrappedParamsFunc = PromisedArgs<Func>
    /* type WrappedParamsFunc =
       (id: Promise<number>, guid: Promise<string | number>) => string | number */
    
    let func: WrappedParamsFunc = (
      id: Promise<number>,
      guid: Promise<number | string>
    ) => 'TestString';
    

    Looks good!


    By the way, there's an bug in TypeScript, reported at microsoft/TypeScript#27995, which might have prevented you from finding this solution yourself. It turns out to be important that you are mapping over the keys of an arraylike generic type parameter. If you try to map over a specific type, the mapping will fallback to iterating over all of the keys, even the array methods and stuff:

    type Foo = [1, 2, 3]
    type Bar = { [I in keyof Foo]: Promise<Foo[I]> }; 
    /* type Bar = {
        [x: number]: Promise<3 | 1 | 2>;
        0: Promise<1>;
        1: Promise<2>;
        2: Promise<3>;
        length: Promise<3>;
        toString: Promise<() => string>; 
        ...
    */
    

    Since Foo is a specific type and not a type parameter, you get the mess above. Your FuncParams is a specific type like Foo, and you tried to clean up the mess manually, which didn't quite work. Oh well!

    Playground link to code