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