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'>]
.
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!