Typescript nyckel-värde-relation som bevarar Object.entries-typ


Typingen för Object.entries som tillhandahålls av TypeScript har returtypen [string, T][] men jag letar efter en generisk typ Entries<O> för att representera returvärdet av denna funktion som behåller relationen mellan nycklarna och värdena.

T.ex. när man har en objekttyp som

type Obj = {
    a: number,
    b: string,
    c: number
}

Jag letar efter en typ Entries<O> som resulterar i en av typerna nedan (eller något liknande) när den ges Obj:

(["a", number] | ["b", string] | ["c", number])[]
[["a", number], ["b", string], ["c", number]]
(["a" | "c", number] | ["b", string])[]

Att detta inte är korrekt för alla användningsfall av Object.entries (se här) är inget problem för mitt specifika fall.

Försökt och misslyckad lösning:

type Entries<O>=[keyof O, O[keyof O]][] fungerar inte för detta eftersom det bara bevarar de möjliga nycklarna och värdena men inte relationen mellan dem, då Entries<Obj> blir ["a" | "b" | "c", number | string].

type Entry<O, K extends keyof O> = [K, O[K]]
type Entries<O> = Entry<O, keyof O>[]

Här fungerar definitionen av Entry som förväntat t.ex. Entry<Obj, "a"> är ["a", number] men tillämpningen av den i den andra raden med keyof O som den andra typvariabeln leder återigen till samma resultat som det första försöket.

Svar #1

Här är en lösning, men var försiktig när du använder detta som returtyp för Object.entries; det är inte alltid säkert att göra det (se nedan).


När du vill para varje nyckel med något som är beroende av den nyckelns typ, använd en mappad typ:

type Entries<T> = {
    [K in keyof T]: [K, T[K]];
}[keyof T][];

type Test = Entries<Obj>;
// (["a", number] | ["b", string] | ["c", number])[]

En alternativ lösning som använder en distributiv villkorstyp: detta kräver att K är en "naken typ-parameter", därav den extra valfria generiska parametern.

type Entries2<T, K extends keyof T = keyof T> =
    (K extends unknown ? [K, T[K]] : never)[]

Den andra versionen, som har en tuple-typ som innehåller egenskaperna istället för en union, är mycket svårare att konstruera; det är möjligt att konvertera en union till en tuple men du borde i princip inte göra det.

Den tredje versionen är hanterbar, men lite mer komplicerad än den första versionen: du behöver PickByValue från detta svar.

type Entries3<T> = {
    [K in keyof T]: [keyof PickByValue<T, T[K]>, T[K]]
}[keyof T][];

type Test3 = Entries3<Obj>;
// (["a" | "c", number] | ["b", string])[]

Lekplatslänk


Jag antar att jag också borde förklara varför Typescript inte ger en starkare typ till Object.entries. När du har en typ som type Obj={a: number, b: string, c: number}, är det endast garanterat att ett värde har dessa egenskaper; det är inte garanterat att värdet inte också har andra egenskaper. Till exempel är värdet {a: 1, b: 'foo', c: 2, d: false} tilldelningsbart till typen Obj (bortsett från kontroll av överflödiga egenskaper för objektliteraler).

I det här fallet skulle Object.entries returnera en array som innehåller elementet ['d', false]. Typen Entries<Obj> säger att detta inte kan hända, men det kan faktiskt hända; så Entries<T> är inte en säker returtyp för Object.entries i allmänhet. Du bör endast använda lösningen ovan med Object.entries när du själv vet att värdena inte kommer att ha några extra egenskaper; Typescript kommer inte att kontrollera detta åt dig.

Svar #2

För närvarande har ett riktigt bra hjälpbibliotek som kallas type-fest introducerats för att inkludera denna funktionalitet för dig, bland annat, i form av Entries. Du kan använda det så här:

import { Entries } from 'type-fest';

Object.entries(obj) as Entries<typeof obj>;

Redigera: Om du vill att Object.entries ska ha denna typ som standard:

declare global {
  interface ObjectConstructor {
    entries<T extends object>(o: T): Entries<T>
  }  
}

Svar #3

De svar som fungerade bäst för mig var en kombination av två från denna tråd:

type Entries<T> = {
    [K in keyof T]-?: [K, T[K]];
}[keyof T][];

från @kaya3

och

const getEntries = <T extends object>(obj: T) => Object.entries(obj) as Entries<T>;

från @minlare

Då är ett exempel på användning att anropa wrapper-funktionen getEntries(obj) istället för Object.entries(obj) direkt.

Tack till er båda för de utmärkta svaren.

Svar #4

// utils/objectEntries.ts

export { objectEntries }

// https://stackoverflow.com/questions/60141960/typescript-key-value-relation-preserving-object-entries-type/75337277#75337277

type ValueOf<T> = T[keyof T]
type Entries<T> = [keyof T, ValueOf<T>][]

// Same as `Object.entries()` but with type inference
function objectEntries<T extends object>(obj: T): Entries<T> {
  return Object.entries(obj) as Entries<T>
}

Relaterade artiklar