Home > Back-end >  Getting keys from constant object literals
Getting keys from constant object literals

Time:10-14

Consider the following:

const foobar = [
  {
    name: 'foo',
    map: { a: {}, }
  },
  {
    name: 'bar',
    map: { b: {}, }
  }
];

type FoobarMapItem = keyof typeof foobar[number]['map']

Here, I get type FoobarMapItem = "a" | "b". However, if I add as const:

const foobar = [
  {
    name: 'foo',
    map: { a: {}, }
  },
  {
    name: 'bar',
    map: { b: {}, }
  }
] as const;

type FoobarMapItem = keyof typeof foobar[number]['map']

The type is now type FoobarMapItem = never (Playground).

Why do readonly (const) objects behave this way? Is there a way to still extract these keys from foobar[number]['map']?

It seems relevant that, if the sub-properties of the map property are the same across elements in foobar, I no longer get never as my type, similar to the first example.

CodePudding user response:

Let's simplify this a bit:

const obj = [
  { a: 'a string' },
  { b: 'b string' }
]

type A = typeof obj[number]
type B = keyof A // 'a' | 'b'

The IDE here tells us that obj is inferred to be of type:

const obj: ({
    a: string;
    b?: undefined;
} | {
    b: string;
    a?: undefined;
})[]

So that means an array that has items that have a and b keys, where one exists and the other is either omitted or set to undefined.

Typescript is assuming you meant an array of unknown length full of object that have one key with a value, and one undefined key.


At this point, I want to mention this type, which I will get back to in a minute, but keep it in mind.

type IAmNever = keyof ({ a: 1 } | { b: 2 }) // never

Now how about this object?

const objAsConst = [
  { a: 'a string' },
  { b: 'b string' }
] as const

Is inferred to be of type:

const objAsConst: readonly [{
    readonly a: "a string";
}, {
    readonly b: "b string";
}]

Typescript now doesn't have to assume anything, and it locks this down tight to be the exact shape of the literal value. It knows you have an array of a known length of 2, where the first object has only an a property, and the second object has only a b property. No other keys are allowed to exist, even if they are assigned to undefined.

Note that each item no longer has the a?: undefined entries. Typescript won't even let you access them to assign undefined, because it knows for sure those keys don't exist.

So type C here is:

type C = {
    readonly a: "a string";
} | {
    readonly b: "b string";
}

This is the same as the IAmNever situation above.

When you keyof a union type, it returns the keys that are in common with all members of that union. In this case there are no keys in common so you get never.

Playground


How do you do it anyway?

You could use a union to intersection generic type to to merge all union members and then get the keys from that.

That means:

type T = UnionToIntersection<{a:1} | {b:2}> // { a: 1, b: 2 }

Now you have a type of a single object and getting the keys is easy.

type UnionToIntersection<U> = 
  (U extends any ? (k: U)=>void : never) extends ((k: infer I)=>void) ? I : never


const objAsConst = [
  { a: 'a string' },
  { b: 'b string' }
] as const

type C = UnionToIntersection<typeof objAsConst[number]>
type D = keyof C // 'a' | 'b'

Playground

I'm not super sure how useful that is though, since you would not able to index objectAsConst with those keys since typescript can't guarantee that each object supports access by all those keys.

  • Related