88

The typing for Object.entries provided by typescript has the return type [string, T][] but I am searching for a generic type Entries<O> to represent the return value of this function that keeps the relationship between the keys and the values.

Eg. when having an object type like

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

I'm looking for a type Entries<O> that results in one of the types below (or something similar) when provided with Obj:

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

That this isn't correct for all use cases of Object.entries (see here) is no problem for my specific case.


Tried and failed solution:

type Entries<O> = [keyof O, O[keyof O]][] doesn't work for this as it only preserves the possible keys and values but not the relationship between these as Entries<Obj> is ["a" | "b" | "c", number | string].

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

Here the definition of Entry works as expected eg. Entry<Obj, "a"> is ["a", number] but the application of it in the second line with keyof O as the second type variable leads again to the same result as the first try.

Malte Laukötter
  • 1,073
  • 1
  • 7
  • 7

6 Answers6

86

Here's a solution, but beware when using this as a return type for Object.entries; it is not always safe to do that (see below).


When you want to pair each key with something dependent on that key's type, use a mapped type:

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

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

The second version, which has a tuple type containing the properties instead of a union, is much harder to construct; it is possible to convert a union to a tuple but you basically shouldn't do it.

The third version is manageable, but a bit more complicated than the first version: you need PickByValue from this answer.

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])[]

Playground Link


I guess I should also explain why Typescript doesn't give a stronger type to Object.entries. When you have a type like type Obj = {a: number, b: string, c: number}, it's only guaranteed that a value has those properties; it is not guaranteed that the value does not also have other properties. For example, the value {a: 1, b: 'foo', c: 2, d: false} is assignable to the type Obj (excess property checking for object literals aside).

In this case Object.entries would return an array containing the element ['d', false]. The type Entries<Obj> says this cannot happen, but in fact it can happen; so Entries<T> is not a sound return type for Object.entries in general. You should only use the above solution with Object.entries when you yourself know that the values will have no excess properties; Typescript won't check this for you.

Richard Ayotte
  • 5,021
  • 1
  • 36
  • 34
kaya3
  • 47,440
  • 4
  • 68
  • 97
  • 3
    Well while this may be a true motivation, they easily could have return type as `Entries & [string, T][]`. It might be not valid TS as I'm not very goot at typelevel, but the idea they could preserve that type T has *at least* these properties and possibly some more (like `d` in your example). It would be then absolutely possible to construct an object from these values and guarantee that it still in valid shape for type T – Alex Zhukovskiy Jan 11 '22 at 22:24
  • is this better than type-fest's [Entries](https://github.com/sindresorhus/type-fest/blob/60a213fff845d44c03923569e503f7f2f3b70d02/source/entries.d.ts#L57)? – Sang Nov 26 '22 at 13:01
  • @transang I don't know anything about that library; there is another answer by Peter Cardenas which mentions it but doesn't go into detail. – kaya3 Nov 26 '22 at 14:31
  • @transang yes! type-fest's `Entries` doesn't respect which values go with which keys and ignores length, e.g. `Entries<{name: string, age: number}>` turned into `["name" | "age", string | number][]` for me. That might be sufficient if your objects are homogeneous and length doesn't matter, but I much prefer this answer's result (`(["name", string] | ["age", number])[]`) - doesn't retain length either, but at least it keeps track of the key-value relationships – jemand771 Feb 15 '23 at 16:25
16

Currently, a really nice utility library called type-fest has been introduced to include this functionality for you, among others, in the form of Entries. You can use it like so:

import { Entries } from 'type-fest';

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

Edit: If you want Object.entries to have this type by default:

declare global {
  interface ObjectConstructor {
    entries<T extends object>(o: T): Entries<T>
  }  
}
Peter Cardenas
  • 508
  • 4
  • 13
  • 1
    Yep. And a way to globally set the return type of `Object.entries` is to have a `.d.ts` file with: `import type { Entries} from 'type-fest' declare global { interface ObjectConstructor { entries(o: T): Entries } }` – blake.vandercar Apr 27 '23 at 20:46
10

I strongly believe the answer to this post should be a combination of two answers on this page:

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

from @kaya3

and

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

from @minlare

Thank you both for the excellent answers.

Robert Rendell
  • 1,658
  • 15
  • 18
  • 2
    I would actually add `[K in keyof T]-?: [K, T[K]]` but this is exactly what i use. If you dont have the `-?` then an object using all partial properties would return any as the key values. – Braden Rockwell Napier Apr 29 '23 at 17:59
5
// 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>
}
brillout
  • 7,804
  • 11
  • 72
  • 84
4

Following on from Peter Cardenas answer, I created a helper function as well

const getEntries = <T extends object>(obj: T) => Object.entries(obj) as Entries<T>;
minlare
  • 2,454
  • 25
  • 46
-3

we can make dedicated function as below:

depictObjectKeyType<O>(o: O) {
    return Object.keys(o) as (keyof O)[];
}

depictEntriesKeyType<T>(obj: T): Entries<T> {
    return Object.entries(obj) as any;
}

and use as:

this.depictEntriesKeyType(data).forEach(....
Dolly
  • 2,213
  • 16
  • 38
  • 5
    This answer does not define the `Entries` type as the OP requested, and suggests using `any` to overcome a lack of typing altogether. Using `any` is always the last resort. I don't understand why it has 4 votes... – JHH Jul 11 '22 at 06:12
  • Hey @JHH, Thanks you for the suggestion. But, I believe that one should be aware of all the solutions and also for the developers working with TS in initial phase, might need this. Stack-overflow is the perfect place where we can have solutions from different background of developers with various different perspective. Pick what suits your need. Thanks! Have a nice day. – Dolly Jul 11 '22 at 14:16
  • "Pick what suits your need", the one that doesn't use `any` – airtonix Aug 17 '22 at 01:12
  • 1
    In this case the `as any` is really not a big deal, as it's only affecting the type returned from `Object.entries` so that it doesn't complain about it not matching `Entries`. The return type of the function is going to replace the `any` anyway. If you really don't like it though, you can easily replace it with `as Entries` instead.. – Jason Kohles Sep 20 '22 at 13:49