typescript - Hämta nycklar från konstanta objektliteraler


Tänk på följande:

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

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

Här får jag type FoobarMapItem="a" | "b". Men om jag lägger till as const:

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

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

Typen är nu type FoobarMapItem=never (Playground).

Varför beter sig readonly (const) objekt på detta sätt?
Finns det ett sätt att ändå extrahera dessa nycklar från det readonly foobar[number]['map']?

Det verkar relevant att, om underegenskaperna för map-egenskapen är desamma över element i foobar, får jag inte längre never som min typ, liknande det första exemplet.

Svar #1

Låt oss förenkla detta lite:

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

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

IDE:n här berättar för oss att obj antas vara av typen:

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

Så det betyder en array som har objekt som har a och b nycklar, där den ena finns och den andra antingen utelämnas eller sätts till undefined.

Typescript antar att du menade en array av okänd längd full av objekt som har en nyckel med ett värde och en odefinierad nyckel.


Vid det här laget vill jag nämna denna typ, som jag kommer att återkomma till om ett ögonblick, men håll det i åtanke.

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

Vad sägs om detta föremål?

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

Antas vara av typen:

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

Typescript behöver nu inte anta något, och det låser detta hårt till att vara den exakta formen av det literala värdet. Det vet att du har en array med en känd längd på 2, där det första objektet har endast en a-egenskap, och det andra objektet har endast en b-egenskap. Inga andra nycklar får finnas, även om de tilldelas undefined.

Observera att varje objekt inte längre har a?: undefined-poster. Typescript tillåter dig inte ens att komma åt dem för att tilldela undefined, eftersom det vet säkert att dessa nycklar inte finns.

Så typen C här är:

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

Detta är samma som IAmNever-situationen ovan.

När du använder keyof på en unionstyp returnerar det de nycklar som är gemensamma för alla medlemmar i den unionen. I det här fallet finns det inga nycklar som är gemensamma, så du får never.

Lekplats


Hur gör du det egentligen?

Du kan använda en union till intersections generiska typ för att slå samman alla union-medlemmar och sedan få nycklarna från det.

Det betyder:

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

Nu har du en typ av ett enda objekt och att få nycklarna är enkelt.

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'

Lekplats

Jag är inte helt säker på hur användbart det är dock, eftersom du inte skulle kunna indexera objectAsConst med de nycklarna eftersom TypeScript inte kan garantera att varje objekt stöder åtkomst med alla de nycklarna.