126

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.

6 Answers 6

124

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

An alternative solution using a distributive conditional type: this requires K to be a "naked type parameter", hence the extra optional generic parameter.

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

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.

Sign up to request clarification or add additional context in comments.

11 Comments

Well while this may be a true motivation, they easily could have return type as Entries<T> & [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
is this better than type-fest's Entries?
@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.
@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
@JohnMiller In English, the required type is "an array of ordered pairs [K, V] where K is each key and V is its corresponding value type", which is about as long and unwieldly as the Typescript version, so perhaps we should stop using English too.
|
46

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

3 Comments

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<T extends object>(o: T): Entries<T> } }
@blake.vandercar great point! Might be useful for Object.fromEntries add fromEntries<T extends readonly (readonly [string, unknown])[]>(o: T): {[K in T[number][0]]: Extract<T[number], [K, unknown]>[1]};
This is presently buggy with objects due to a TypeScript change github.com/sindresorhus/type-fest/issues/649 . I had to extract ObjectEntries from the lib te get the right types
23

The answers that worked best for me were a combination of two from this thread:

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

from @kaya3

and

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

from @minlare (and @brillout)

Then an example of usage is invoking the wrapper function objectEntries(obj) instead of Object.entries(obj) directly.

Thank you both for the excellent answers.

2 Comments

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.
As a minor improvement, you can assign const getEntries = Object.entries as <T>(obj: T) => Entries<T>; directly, without an extra level of indirection. Or, if you want this typing for Object.entries itself you can declare that by interface merging. Playground Link. But see my answer for why you might not want to.
12
// 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>
}

Comments

6

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

Comments

-4

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(....

4 Comments

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...
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.
"Pick what suits your need", the one that doesn't use any 🙌 🤝
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<T>. 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<T> instead..

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.