Reputation: 7542
Let's say I am dealing with objects corresponding to the following interface:
interface Foo {
getCount(): number;
doSomething(): boolean;
}
It only has functions on it, and none of the functions are async. However, I don't always have synchronous access to the object and in some cases will be dealing with an asynchronous version, in which all of the function return values are wrapped in Promises. Like so:
interface AsyncFoo {
getCount(): Promise<number>;
doSomething(): Promise<boolean>;
}
I'm trying to create a Typescript mapped type to represent this sort of transformation, as I have a large number of object interfaces and do not want to simply duplicate each interface and end up with both interface [name]
and interface Async[name]
and all the method prototypes duplicated.
My first thought was that maybe I could modify the interfaces like this:
type Self<T> = T;
interface Foo<S = Self> {
getCount(): S<number>;
doSomething(): S<boolean;
}
type AsyncFoo = Foo<Promise>;
But both Self
and Promise
require generics to be statically given when I use them, rather than being able to use them in this backwards fashion.
So next I attempted to create some sort of mapped type such as:
type Promisify<T> = {[K in keyof T]: Promise<T[K]>}
But of course this wraps each entire method of the interface in a Promise, rather than just the return value, giving me:
type PromisifiedFoo = {
getCount: Promise<() => number>;
doSomething: Promise<() => boolean>;
}
I've attempted to expand on this by using scoping on the generic T
of Promisify
like:
type Promisify<T extends {[key: string]: <S>() => S}> = ...
But I can't seem to get it to all fit together.
And so now I'm here. Is there any way for me to build a type (mapped or otherwise) representing this "Promisify" transformation onto the return values of a type?
Upvotes: 4
Views: 873
Reputation: 1217
With the new Conditional Types in Typescript 2.8 you can do the following:
// Generic Function definition
type AnyFunction = (...args: any[]) => any;
// Extracts the type if wrapped by a Promise
type Unpacked<T> = T extends Promise<infer U> ? U : T;
type PromisifiedFunction<T extends AnyFunction> =
T extends () => infer U ? () => Promise<Unpacked<U>> :
T extends (a1: infer A1) => infer U ? (a1: A1) => Promise<Unpacked<U>> :
T extends (a1: infer A1, a2: infer A2) => infer U ? (a1: A1, a2: A2) => Promise<Unpacked<U>> :
T extends (a1: infer A1, a2: infer A2, a3: infer A3) => infer U ? (a1: A1, a2: A2, a3: A3) => Promise<Unpacked<U>> :
// ...
T extends (...args: any[]) => infer U ? (...args: any[]) => Promise<Unpacked<U>> : T;
type Promisified<T> = {
[K in keyof T]: T[K] extends AnyFunction ? PromisifiedFunction<T[K]> : never
}
Example:
interface HelloService {
/**
* Greets the given name
* @param name
*/
greet(name: string): string;
}
function createRemoteService<T>(): Promisified<T> { /*...*/ }
const hello = createRemoteService<HelloService>();
// typeof hello = Promisified<HelloService>
hello.greet("world").then(str => { /*...*/ })
// typeof hello.greet = (a1: string) => Promise<string>
Upvotes: 5
Reputation: 328292
One way to attack this is to make a generic version of Foo
which can be specialized into either the synchronous version or the asynchronous version.
type MaybePromise<T, B extends 'plain' | 'promise'> = {
plain: T,
promise: Promise<T>
}[B]
The type MaybePromise<T,'plain'>
is equal to T
, and MaybePromise<T, 'promise'>
is equal to Promise<T>
. Now you can describe the generic Foo
:
type GenericFoo<B extends 'plain' | 'promise'> = {
getCount(): MaybePromise<number, B>;
doSomething(): MaybePromise<boolean, B>;
}
And finally specialize it:
interface Foo extends GenericFoo<'plain'> { }
interface AsyncFoo extends GenericFoo<'promise'> { }
This should behave how you expect:
declare const f: Foo;
if (f.doSomething()) {
console.log(2 + f.getCount());
}
declare const aF: AsyncFoo;
aF.doSomething().then(b => {
if (b) {
aF.getCount().then(n => {
console.log(2 + n);
})
}
});
Hope that helps. Good luck!
Upvotes: 1