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>
}
