Home > database >  Typescript - map results of Promise.allSettled back to object and retain types
Typescript - map results of Promise.allSettled back to object and retain types

Time:10-17

I'm trying to take an object of promises, and create a function that resolves them at the same time, where the result of each one is independent of the others (basically Promise.allSettled). It should then return the results in the same shape that they were originally in (the object of keys and the values are the resolved promises).

I've got it working logic wise, but I'm struggling to get the types working. After it runs through Promise.allSettled it loses the types returned by those functions, and I'm also struggling to build the object that gets returned from loadData with the types. Any tips would be appreciated..

interface DataToFetch {
    [propKey: string]: ()=> Promise<any> // Assuming 'any' is incorrect, just put it as this temporarily to make more of the TS compile
}

const getFoo = async (foo: string): Promise<string[]> => {
    return ["a","b","c"]
}

const getBar = async (): Promise<number[]> => {
    return [1,2,3]
}

const loadData = async <T extends DataToFetch>(dataToFetch: T) => {
    const loadedData = await Promise.allSettled(Object.values(dataToFetch)); // This currently loses the types returned by the functions in dataToFetch
    const returnObj = {}; // Not sure how to set the type on this object so that it matches the keys passed in (it's basically the same as T but with resolved promise values.. maybe it could be done with 'Awaited', or fix the types returned by Promise.allSettled and somehow use those)

    Object.keys(dataToFetch).forEach((key,i) => {
        returnObj[key] = loadedData[i];
    });
   return returnObj; 
}

const loader = async () => {
    const data = await loadData({
        foo: getFoo('abc'),
        bar: getBar()
    });

    /** Type for 'data' should look like this:
    {
        foo: string[],
        bar: number[]
    }
    */
    return data;
}

CodePudding user response:

I think the easiest way here is to avoid giving the intermediate object any type and just let the TS compiler infer it. You can do that by annotating the return type - just map the DataToFetch interface using the built-in Awaited utility type.

type FetchMap = {
    [key: string]: Promise<any>
}

type Fetched<T extends FetchMap>={
    [key in keyof T]: Awaited<T[key]>
}

const loadData = async <T extends FetchMap>(fetchers: T): Promise<Fetched<T>> => {
    return Object.fromEntries(await Promise.all(
        Object.entries(fetchers).map(([key, promise]) => promise.then(r => [key, r]))
    ))
}

Here is a working example in a TS playground. It uses Object.entries to deconstruct the object and Object.fromEntries to create a new one after awaiting the result which also preserves the property types.

CodePudding user response:

Solved it with the following:

type PlainObj = Record<string, unknown>;
export type PromisesMap<T extends PlainObj> = {
  [P in keyof T]: Promise<T[P]> | T[P];
};

const loadData = async <T extends PlainObj>(
  promisesMap: PromisesMap<T>
): Promise<T> => {
  const resolved = await Promise.allSettled(Object.values(promisesMap));
  const toReturn: {[key: string]: unknown} = {};
  Object.keys(promisesMap).forEach((key, index)=> {
    toReturn[key] = resolved[index];
  });
  return toReturn as T;
}

const getFoo = async (foo: string): Promise<string[]> => {
    return ["a","b","c"]
}

const getBar = async (): Promise<number[]> => {
    return [1,2,3]
}

const loader = async () => {
    const data = await loadData({
        foo: getFoo('abc'),
        bar: getBar()
    });
    return data;
}
  • Related