0

Example code:

type I = 'a' | 'b' | 'c';

const is: Record<I, null> = { 'a': null, 'b': null, 'c': null}

// replace with any async processing function of your choice
function asyncRandomize(): Promise<number> {
    return new Promise(resolve => setTimeout(() => resolve(Math.random()), 1000))
}

const irandomized: Promise<[I, number][]> = Promise.all(
    Object.keys(is).map((i: I) => 
        asyncRandomize()
            .then((v): [I, number] => [i, v])
    )
)

Is there a way to make irandomized have type Promise<Record<I, number>> instead of Promise<[I, number][]> without going through any?

Carl Patenaude Poulin
  • 6,238
  • 5
  • 24
  • 46

1 Answers1

3

I think that you will need a function that works like Object.fromEntries(), although the TypeScript typings for that function will result in something a little too wide for your liking (you might get Record<string, number> instead of Record<I, number>. If you don't mind using type assertions you can just do that:

const irandomized  = Promise.all(
   (Object.keys(is) as I[]).map((i: I) =>
      asyncRandomize()
         .then((v): [I, number] => [i, v])
   )
).then(z => Object.fromEntries(z)) as Promise<Record<I, number>>;

You're not using any there.


If you don't want to use type assertions in your code, you can make your own function like Object.fromEntries() whose typings are a little more specific (although type assertions or the like need to happen inside the implementation of that function). Here's one possible function that might work for you:

type Entry = readonly [PropertyKey, any];
type ExtractSupertype<T, U> = T extends any ? [U] extends [T] ? T : never : never;
function fromEntries<E extends readonly Entry[]>(entries: E): {
   [K in E[number][0]]: ExtractSupertype<E[number], readonly [K, any]>[1] // TS 4.0-
   // [K in E[number] as K[0]]: K[1] // TS 4.1+, easier syntax
} {
   const ret: any = {};
   for (let entry of entries) {
      ret[entry[0]] = entry[1];
   }
   return ret;
}

Note that when TypeScript 4.1 is released and introduces mapped type as clauses, the ExtractSupertype formulation will be unnecessary.

You can take that and hide in a library somewhere. The idea is that fromEntries() should be able to turn a strongly-typed array or tuple of entries and turn it into a strongly-typed object:

const foo = fromEntries([
   ["x", Math.random()], ["y", new Date()], ["z", Math.random() < 0.5]
] as const);
/* const foo: {
    x: number;
    y: Date;
    z: boolean;
} */

You can see that instead of just getting Record<string, number | Date | boolean>, you actually get the specific properties associated with specific types.

Armed with that, you can do something like this with no more unsafe type assertions:

const iKeys = ["a", "b", "c"] as const;
const irandomized = Promise.all(
   iKeys.map((k) =>
      asyncRandomize()
         .then(v => [k, v] as const)
   )
).then(fromEntries);

Note that I changed is to just an array of its keys. You weren't doing anything with its values anyway, and the compiler sees Object.keys(is) as string[] instead of I[] (and for good reason, see this question), so I opted to cut out the middleman and use a strongly-typed tuple of keys instead.

Anyway you can verify that irandomized is of the type you expect:

/* const irandomized: Promise<{
    a: number;
    b: number;
    c: number;
}> */

and that it works as you expect at runtime:

irandomized.then(e => console.log(JSON.stringify(e)));
// {"a":0.9961594084980729,"b":0.015675814053288217,"c":0.1783156372032898}

Playground link to code

jcalz
  • 264,269
  • 27
  • 359
  • 360