6

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

Natalia
  • 61
  • 3

1 Answers1

7

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

jcalz
  • 264,269
  • 27
  • 359
  • 360
  • Hi @jcalz thanks for the answer! I've a follow up question. if `literally` takes a reference of a variable instead of a value, Typescript takes to narrowing again. ` const val = [{ key: "a1" }, { key: "a2" }]; // Array<{key: string}> const val1 = literally(val); type ValueAtKey1 = (typeof val1)[number]["key"]; // "string" ` is there something one can do here? – Arihant Dec 24 '20 at 04:54
  • Hi @jcalz! I just stumbled upon my own question, here in the comments, after 6 months and realised I still don't know how to do that. Could you help me out? – Arihant Jun 23 '21 at 13:31
  • 1
    There's no way to retrieve such information once the compiler has already thrown it away by widening; you need to prevent the widening in the first place. The way to prevent widening is shown in this answer (updated now to show `const` assertions). If that was not prevented, there's not much you can do. Note that a follow-up question in a comment on a year-old answer is not the best place to get attention; if you have a question and you can't find an answer for it, you might want to make your own post asking it. – jcalz Jun 23 '21 at 13:45
  • Understood, thank you for the answer, and thank you for also pointing out that making a new question, if not duplicate, would have been a better place to ask. Appreciate it. – Arihant Jun 24 '21 at 05:06