Reputation: 7265
I have made a class to handle promises in batches and return the result based on the key they were given in. For example if you give it two keys named order
and customer
, each with a promise, it will resolve those promises and return an object with the those keys as the properties and the resolved values as their respective values.
So here is how this class might be used:
const batchPromiseHandler = new BatchPromise();
// getCustomerInfo and getPaymentInfo will give back a promise which resolves into their data
batchPromiseHandler.add('order', getOrderInfo());
batchPromiseHandler.add('customer', getCustomerInfo());
// await to resolve all into result object
const result = await batchPromiseHandler.resolveAll();
console.log(result.order); // <<-- I want to be able to get suggestion order or customer from IDE
console.log(result.customer);
And here is the actual implementation:
type resultDecorator = (data: any[], index: number) => any;
class BatchPromise {
private promiseList: Promise<any>[] = [];
private keyList: string[] = [];
private decoratorList: resultDecorator[] = [];
add(key: string, promise: Promise<any>, decorator?: resultDecorator): void {
if (this.keyList.indexOf(key) !== -1) {
throw new Error(`Key: "${key}" already exists in PromiseLand!`);
}
this.promiseList.push(promise);
this.keyList.push(key);
this.decoratorList.push(decorator);
}
async resolveAll(): Promise<{ [key: string]: any }> { // <<------ here is naive return type
const resolvedArray = await Promise.all(this.promiseList);
const result = {};
for (let index = 0; index < this.promiseList.length; index++) {
const key = this.keyList[index];
result[key] =
typeof this.decoratorList[index] === 'function'
? await this.decoratorList[index](resolvedArray[index], index)
: resolvedArray[index];
}
return result;
}
}
It works fine as expected but I want to be able to get autocomplete for the result from resolveAll
function. I don't know how to use dynamic type features of the language, so I just did this:
Promise<{ [key: string]: any }>
How can I refactor it to be able to get for example order
or customer
suggested to me by the IDE?
Upvotes: 1
Views: 460
Reputation: 327744
The problem here is that the type BatchPromise
doesn't know anything about the particular keys and values that it is holding onto. If you want it to keep track of this, it needs to be a generic type like BatchPromise<T>
, where T
is the object type representing the key-to-value mapping returned in resolveAll()
.
class BatchPromise<T extends object = {}> {
...
async resolveAll(): Promise<T> { ... }
}
So every time you call add()
, you'd be changing from a BatchPromise<T>
to a BatchPromise<T & Record<K, V>>
where K
and V
are your key and value types, respectively. This hits a bit of a snag: the type system doesn't support arbitrarily changing the type of an existing object. If you're careful, you could write BatchPromise
so that add()
is seen as narrowing the type, which is supported; this would need to use an assertion function (so that add
returns asserts this is BatchPromise<T & Record<K, V>>
). But assertion functions are not very easy to use right now, (see microsoft/TypeScript#33622), so I'm not going to provide such a solution.
Instead of making the bp.add()
method change the type of bp
, things become a lot nicer in the type system if bp.add()
returns a BatchPromise
object of the modified type:
add<K extends string, V>(
key: K, promise: Promise<V>, decorator?: resultDecorator
): BatchPromise<T & Record<K, V>> {
...
return this as BatchPromise<T & Record<K, V>>;
}
In order for that to work, you'd want to change how you call add()
to incorporate method chaining instead of multiple statements:
const batchPromiseHandler = new BatchPromise()
.add('order', getOrderInfo())
.add('customer', getCustomerInfo());
That way your batchPromiseHandler
would be of a type like BatchPromise<{order: OrderInfo, customer: CustomerInfo}>
.
Let's see if that works:
const result = await batchPromiseHandler.resolveAll();
result.customer; // CustomerInfo
result.order; // OrderInfo
result.randomThing; // error!
// Property 'randomThing' does not exist on type
// 'Record<"order", OrderInfo> & Record<"customer", CustomerInfo>'
Looks good. And you can verify (via the Playground link below) that an IDE's IntelliSense will be able to prompt that result
has a customer
and an order
property.
Okay, hope that gives you a way forward. Good luck!
Upvotes: 2