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
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
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With