Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Extract string union from array of objects

I have following object

[{
    "key": "a1",
    ...
}, {
    "key": "a2",
    ...
}, ...]

Is it possible to extract union type "a1" | "a2" | ... from this object? I am aware that it is possible to extract it from ['a1', 'a2', ...] by using tuple API, which was presented here TypeScript String Union to String Array, but I can't figure it out for object array

like image 378
Natalia Avatar asked Jan 02 '19 16:01

Natalia


1 Answers

Basically you just want to do a lookup of the "key" property of the elements of the array, where the elements of an array can be found by looking up its number property. Unfortunately, the hard part is getting that to show up as anything but "string".

const val = [{ key: "a1" }, { key: "a2" }]; // Array<{key: string}>
type ValueAtKey = (typeof val)[number]["key"]; // string 🙁

That's because the compiler infers val to just be an array of objects with a string value. The compiler uses some heuristics to determine when to widen literals, and in the above case, the exact string literals have been widened to string.

UPDATE FOR TS3.4+

As of TypeScript 3.4, the recommended way to make the compiler infer the most specific type for an object/array literal is to use a const assertion:

const val = [{ key: "a1" }, { key: "a2" }] as const;
// const val: readonly [{  readonly key: "a1"; }, { readonly key: "a2"; }] 
type ValueAtKey = (typeof val)[number]["key"]; // "a1" | "a2" 🙂

ORIGINAL PRE-TS3.4 ANSWER

Before TS3.4, you had to do something else. One of the ways to hint to the compiler that a value like "a1" should stay narrowed to "a1" instead of widened to string is to have the value match a type constrained to string (or a union containing it). The following is a helper function I sometimes use to do this:

type Narrowable = 
  string | number | boolean | symbol | object | 
  null | undefined | void | ((...args: any[]) => any) | {};

const literally = <T extends { [k: string]: V | T } | Array<{ [k: string]: V | T }>,
  V extends Narrowable>(t: T) => t;

The literally() function just returns its argument, but the type tends to be narrower. Yes, it's ugly.

Now you can do:

const val = literally([{ key: "a1" }, { key: "a2" }]); // Array<{key: "a1"}|{key: "a2"}>
type ValueAtKey = (typeof val)[number]["key"]; // "a1" | "a2" 🙂

The val object is the same at runtime, but the TypeScript compiler now sees it as an array of values of type {key: "a1"} or {key: "a2"}. Then the lookup done for ValueAtKey gives you the union type you're looking for.

(Note that I assume you don't care about the ordering of val here. That is, you are fine treating it as an array instead of as a tuple. Since the union type "a1" | "a2" doesn't have an inherent ordering, then the array should be sufficient.)


Playground link to code

like image 146
jcalz Avatar answered Oct 27 '22 10:10

jcalz