简体   繁体   中英

TypeScript Typed Object Output With Same Keys as Input

I'm trying to write a function which resolves all Promise values of an object:

const resolveObject = async (obj) => // pure JavaScript implementation
  Object.fromEntries( // turn the array of tuples with key-value pairs back into an object
    (await Promise.all(Object.values(obj))).map((el, i) => [ // Promise.all() the values, then create key-value pair tuple
      Object.keys(obj)[i], // tuple contains the original object key
      el, // tuple contains the resolved object value
    ]) // zips the split key/value arrays back into one object
  );

The implementation works fine. Below is a usage example:

/* example input */
const exampleInput = { // <- { foo: Promise<string>, bar: Promise<{ test: number }>, baz: boolean }
    foo: Promise.resolve("Hello World"),
    bar: Promise.resolve({
        test: 1234
    }),
    baz: false
}
const expectedOutput = resolveObject(exampleInput) // <- { foo: string, bar: { test: number }, baz: boolean }
/* expected output: strongly typed object with same keys and no Promise<> wrappers in signature */

This is where things start to fall apart. I'm expecting a strongly typed output with a similar signature as the input (just without the Promise wrappers), but instead, I get the following generic object output:

Promise<{ [k: string]: unknown; }>

So I started adding type annotations to the resolveObject function:

const resolveObject = async <T>(
  obj: { [K in keyof T]: Promise<T[K]> }
): Promise<{ [K in keyof T]: T[K] }> => { ... }

Now I receive a type conversion error: type '{ [k: string]: unknown; }' is not assignable to type '{ [K in keyof T]: T[K]; }' type '{ [k: string]: unknown; }' is not assignable to type '{ [K in keyof T]: T[K]; }'
I'm fairly new to TypeScript and don't really know what to do next (I know how to diagnose the error message, but I'm pretty sure that something with my type annotation/function signature is wrong). How can I achieve what I'm looking for?

If you would like the type of resolveObject() 's output to be dependent on the type of its input, you need to give it a generic function signature; there's no facility in TypeScript to just infer that a function has such a generic signature.

The intended signature is something like this:

/* const resolveObject: <T extends object>(
  obj: { [K in keyof T]: T[K] | Promise<T[K]>; }
) => Promise<T> */

That means the output type is Promise<T> for some generic type T determined by the input obj . In particular, obj is of a mapped type where each property of T at key K , namely T[K] , is either left alone ( T[K] ), or (the union | ) wrapped in Promise ( Promise<T[K]> ). It turns out that the compiler is able to figure out T from the type of obj via inference from mapped types .


Unfortunately, there's not much hope that the compiler would be able to follow the particular implementation you've got in order to verify that the returned value conforms to the type signature. The typings for Object.fromEntries() and Object.keys() are not specific enough to infer what you want, and even if it could, the correlation between the index i of Object.keys() and Object.values() is not easily representable. Instead of trying to figure out how to get the compiler to understand that the implementation is type safe, it is more expedient to just be very careful that we've done it right and then assert that we have done so:

const resolveObject = async <T extends object>(
  obj: { [K in keyof T]: Promise<T[K]> | T[K] }
) =>
  Object.fromEntries(
    (await Promise.all(Object.values(obj))).map((el, i) => [
      Object.keys(obj)[i],
      el,
    ])
  ) as T; // assertion here

We've asserted that Object.fromEntries(...) results in a value of type T . If we made a mistake then the compiler won't notice; type assertions shift the burden of maintaining type safety from the compiler to the developer. But in this case, we have a single type assertion in the implementation of resolveObject() , and callers don't need to worry about it:

const exampleInput = {
  foo: Promise.resolve("Hello World"),
  bar: Promise.resolve({
    test: 1234
  }),
  baz: false
}
const expectedOutput = resolveObject(exampleInput);
/* const expectedOutput: Promise<{
    foo: string;
    bar: {
        test: number;
    };
    baz: boolean;
}> */

You can see that expectedOutput 's type has been inferred by the compiler to be what you expect: a Promise<> of an object type with known keys and property types:

expectedOutput.then(x => console.log(x.bar.test.toFixed(2))) // 1234.00

Looks good.

Playground link to code

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM