More powerful type definitions for Object.entries()
Tags: typescript
Published: 2022-03-10 07:53:07

---
title: More powerful type definitions for Object.entries()
published: true
description: 
tags: typescript
---

## Introduction

`Object.entries<T>()` has the following type definition (typing) as standard in TypeScript and returns an array of type `[string, T]`.

[https://github.com/microsoft/TypeScript/blob/v4.6.2/lib/lib.es2017.object.d.ts#L34-L44](https://github.com/microsoft/TypeScript/blob/v4.6.2/lib/lib.es2017.object.d.ts#L34-L44)

> ```typescript
>     /**
>      * Returns an array of key/values of the enumerable properties of an object
>      * @param o Object that contains the properties and methods. This can be an object that you created or an existing Document Object Model (DOM) object.
>      */
>     entries<T>(o: { [s: string]: T } | ArrayLike<T>): [string, T][];
> 
>     /**
>      * Returns an array of key/values of the enumerable properties of an object
>      * @param o Object that contains the properties and methods. This can be an object that you created or an existing Document Object Model (DOM) object.
>      */
>     entries(o: {}): [string, any][];
> ```

There is nothing wrong with this definition itself, but there are cases where you want to get a type like ``<K = keyof T> [`${K}`, T[K]][]`` from `Object.entries()` when passed an object generated by pure processing without side-effects (*1).

- *1: If it is clear that there are only key-value type combinations defined by type in the object, rather than an object obtained via a side-effect such as the REST API call. (See also #Appendix of this article)

## Type definitions

In such cases, the following type definitions can be defined to get the expected type.

```typescript
type TupleEntry<T extends readonly unknown[], I extends unknown[] = [], R = never> =
  T extends readonly [infer Head, ...infer Tail] ?
    TupleEntry<Tail, [...I, unknown], R | [`${I['length']}`, Head]> :
    R

// eslint-disable-next-line @typescript-eslint/ban-types
type ObjectEntry<T extends {}> =
  // eslint-disable-next-line @typescript-eslint/ban-types
  T extends object ?
    { [K in keyof T]: [K, Required<T>[K]] }[keyof T] extends infer E ?
      E extends [infer K, infer V] ?
        K extends string | number ?
          [`${K}`, V] :
          never :
        never :
      never :
    never

// eslint-disable-next-line @typescript-eslint/ban-types
export type Entry<T extends {}> =
  T extends readonly [unknown, ...unknown[]] ?
    TupleEntry<T> :
    T extends ReadonlyArray<infer U> ?
      [`${number}`, U] :
      ObjectEntry<T>

// eslint-disable-next-line @typescript-eslint/ban-types
export function typedEntries<T extends {}>(object: T): ReadonlyArray<Entry<T>> {
  return Object.entries(object) as unknown as ReadonlyArray<Entry<T>>;
}
```

## Examples of type definition usage

Calling `typedEntries()` function, which wraps `Object.entries()`, returns an array of entry types inferred according to the argument type.

[TypeScript: TS Playground](https://www.typescriptlang.org/play?exactOptionalPropertyTypes=true#code/C4TwDgpgBAKgrmANhAogO2AJxAHhlCAD2AjQBMBnKTCAQzIHs1EQo40BrNBgdzQG0AugBooASQLFSlNp258hUALxQhogErKoaCADcImAHzKAUFFiSS5KjXpMWqgJZoAZgagAJOmVEA6f85umLC0joiCUAD8ZuawCMjoWLgwoYii-P6+YqLsXLxoIlCaAD6qAAYAJADeYvwA5MhoAObAABZ1ggC+ZaJe9ILGAFwx5uomJqCQUADyAEYAVhAAxsCJ2HiW0lRVncZKMfhEVjIMC8vAUSNQVaoA0lDOUBwQIAwusIKDdxoQAI5wjhoZDwhn4t0EEU6-Ger3eMAiRy2D1c7hQl1isTRiOsThRwVuokC7gAahFohiMfdsTIKFhnE0oKU0HAALazdzkikY-iVKq3bqiUlQYZcjE6fTBEWi8XuKVivSyq4yzDjIhgBiYC6TaBrZKbHE7PYHfUyWyMZisfi5eRoPz+a35IRkq7wJCoDDrGBDF0mqjqbz2EAAQUwmFouCJwQAqsZOdzecy2QYBVAoxE5bE5osVrqQarCOrNVAXOwVo4mFBtWQ1o4IBQNtTtrsABSnbPAL4wACUX39dgtIbDuFzXuMVRiNGAcEwaBmZxWvlIdLrrfnwC7UFoVAdfE3foDA9D4ZwI8MhgA3CZOuNtVAAMp05q3F7+pYa6se1gqXOv984WmYPSoizAwDDILQaBniYAD00GqABQFQCBYF0AUN7gNAAByrLspgz4gL+mAfkkWg-ssf6JrhwGgeBkGGDBcE8tUlHJj0SE0ahgjoVMUZoOWaD4YRxHYKRn44EJOB1C4oF1IyUB1LMtCYHU1EoRBUGwaoUkyaptERKU9SKcpumcdx0BZucMAYVo47mLQXwsZgwgxLMkRfAhzTOeYSwOTh7ilOwZAQC4zgQD4MRkG5UAeQyAXkMFoVkFeZlzu2VmQLqolJDgFkrOlED0ZpjjvGQjgULQszIGQki0Cs0xgMA-G0IgAAKmAMJAmogPlVAdY1TAMbE-AAES0MNoiOfpqjDbM43RY+TRTSNSxzY5cmBQlOhkEtw1kHNMVcUV7ykBVVU1XVDVNa17WdaAPVQH1-GDeYI1jRNfmYDts2iAdcnLatH3rfFIVbTte0-QtQNBSDYVcSYb5oLSUAI7SuUXCotmbl8w3SQw40uV8AAsABMXnI18LjNRQEBkwAjF8NzatjjnDVA16dHuyNMLSl4TNZADC3PAGjmXfmJ2pvFziPC2uhWMaNc046Bw1fXNJM7StoiU4g1M7bTc03GagaVhhzMfcN55s3Dt68fxaNaM2MQ3C0UVrRzABkTtQIgwCu4D14bsUXvssAPAQKQXz8I571Jp9bMpbbTAi5+WXrInaBo3L00tADsc7T7ue4V9ECh+HaBzVHH0x0XcMoxcFAMCyEB3iAbJgVoLdt4gzZ1A3Td1F2vPOCQmCU0s0BiC1BgN7OmO0E0EB+7Hl7mGgtBN+5C0r-BjfN63IHhF8AAitAkMlfNTGIGAGGP7okWL2WT9PTBZ69C+FwYO1r03+0Ldb1mDnDKLKAuZAG4EchpRiCYPopkmilPs5oWBgOAWRfsSCjy4GQrRSB5RmIwLYlg0yF9oDp2QSnB+6wwE4DWqUQh6lX7QNjrAwGtCOIQX-lMV0yAyH3xAWJfgMVq4GBMuw1+w0AAMv9ALND1h-OOBlhrEzmnQtC8MhZS1pFw6AKgRq4zVqTa4TwXjY10M1OAEBWadAiFuDRwBea3kFtLLRPCRIUNwBLd4dctFiMkaIJWeNZGiHVn9RRBtqAHgcDCExZiLGWysUAA)

```typescript
type StringKeyRecordEntry = Entry<Record<string, boolean>>
// [string, boolean]

type NumberKeyRecordEntry = Entry<Record<number, boolean>>
// [`${number}`, boolean]

type UnionKeyRecordEntry = Entry<Record<'foo' | 'bar', boolean>>
// ['foo', boolean] | ['bar', boolean]

type ObjectType = {
  a: number,
  b?: string,
  c: number | undefined,
  d?: string | undefined
}

type ObjectTypeEntry = Entry<ObjectType>
// if disabled exactOptionalPropertyTypes option
//   ["a", number] | ["b", string] | ["c", number | undefined] | ["d", string]
// if enabled exactOptionalPropertyTypes option
//   ["a", number] | ["b", string] | ["c", number | undefined] | ["d", string | undefined]

const constObject = {
  a: "foo",
  b: 42,
  c: false,
  1: { type: "number" }
} as const;

type ConstObjectEntry = Entry<typeof constObject>
// ["a", "foo"] | ["b", 42] | ["c", false] | ["1", { readonly type: "number"; }]

type UnionObject = (
  { gt?: number } &
  { lt?: number }
) |
  { between: [number, number] }

type UnionObjectEntry = Entry<UnionObject>
// ["gt", number] | ["lt", number] | ["between", [number, number]]

const someSymbol = Symbol('some');

interface IPerson {
  age?: number;
  name: string;
  [someSymbol] : Date;
}

type InterfaceEntry = Entry<IPerson>
// ["age", number] | ["name", string]

type ArrayEntry = Entry<Array<number>>
// [`${number}`, number]

type ReadonlyArrayEntry = Entry<ReadonlyArray<boolean>>
// [`${number}`, boolean]

type UnionArrayEntry = Entry<Array<number | boolean>>
// [`${number}`, number | boolean]

type TupleArrayEntry = Entry<[string, number, boolean]>
// ["0", string] | ["1", number] | ["2", boolean]

const constTuple = ["foo", 42, { key: "value" }] as const;

type ConstTupleArrayEntry = Entry<typeof constTuple>
// ["0", "foo"] | ["1", 42] | ["2", { readonly key: "value"; }]
```

## Overview of type definitions

The key-value entry type of any object can be obtained by the following type definition.

```typescript
type Entry<T> = { [K in keyof T]: [K, T[K]] }[keyof T]

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

type FooObjectEntry = Entry<FooObject>
// ["a", number] | ["b", string] | ["c", number]
```

However, this type definition will not return the expected type when used on an object of a union type and an object containing optional keys.

```typescript
type Entry<T> = { [K in keyof T]: [K, T[K]] }[keyof T]

type UnionObject =　{ 1: number } |　{ 2: string }

type UnionObjectEntry = Entry<UnionObject>;
// never

type ContainsOptional = {
  a: number,
  b?: string,
  c: number | undefined,
  d?: string | undefined
}

type ContainsOptionalEntry = Entry<ContainsOptional>;
// ["a", number] | ["b", string | undefined] | ["c", number | undefined] | ["d", string | undefined] | undefined
```

So first of all, let's solve the problem that occurs with the union type.

For example, if we define a type like `type SingleTuple<T> = T extends infer A ? [A] : never` and pass a single type or a union type to the generic type `T`, what type will be returned? In particular, what is the correct type when passing a union type to the generic type `T`?

```typescript
type SingleTuple<T> = T extends infer A ? [A] : never

type StringSingleTuple = SingleTuple<string>
// [string]

type UnionSingleTuple = SingleTuple<string | number>
// [string] | [number]
```

The correct answer was `[A] | [B]`, if `T` was union type `A | B`(*2).

- *2: [Distributive Conditional Types](https://www.typescriptlang.org/docs/handbook/2/conditional-types.html#distributive-conditional-types)

Just as an example, if a type definition `T extends infer A` is defined for a generic type `T`, and `T` is a union type, the following `for` statement is added to the type definition.

**pseudo code of this type definition**

```typescript
type T = string | number

let ReturnType = never;

for (t of T) {
  if (t extends infer A) {
    ReturnType = ReturnType | [A];
  } else {
    ReturnType = ReturnType | never;
  }
}

return ReturnType;
// [string] | [number]
```

So we add `extends` that explicitly states that we will only consider the generic type `T` if it is an `object` type, so that even if `T` is a union type, it will return an entry type for each type contained in the union type.

```typescript
type ObjectEntry<T extends {}> =
  T extends object ?
    { [K in keyof T]: [K, T[K]] }[keyof T] :
    never

type UnionObject =　{ 1: number } |　{ 2: string }

type UnionObjectEntry = ObjectEntry<UnionObject>
// [1, number] | [2, string]
```

The reason why the line added to the type definition is not `T extends Record<>` is because it returns `never` if the generic type `T` is an interface. In fact, interface does not satisfy the `Record<>` type. (See also #Appendix of this article)

Next, let's solve the problem that occurs when an optional key is included.

If the key is optional, the resulting entry type contains `undefined`, so we remove it using `Exclude<>`. And then the value type contains `undefined`, so `T` is changed to `Required<T>` to get the value type. However, the type obtained from the type definition changes depending on the setting of `exactOptionalPropertyTypes` in TS Config.

https://www.typescriptlang.org/tsconfig#exactOptionalPropertyTypes

```typescript
type ObjectEntry<T extends {}> =
  T extends object ?
    Exclude<{ [K in keyof T]: [K, Required<T>[K]] }[keyof T], undefined> :
    never

type ContainsOptional = {
  a: number,
  b?: string,
  c: number | undefined,
  d?: string | undefined
}

type ContainsOptionalEntry = ObjectEntry<ContainsOptional>
// if disabled exactOptionalPropertyTypes option
//   ["a", number] | ["b", string] | ["c", number | undefined] | ["d", string]

// if enabled exactOptionalPropertyTypes option
//   ["a", number] | ["b", string] | ["c", number | undefined] | ["d", string | undefined]
```

Finally, the Entry type we are expecting is a type whose key is String Literal, so we convert it with [Template Literal Types](https://www.typescriptlang.org/docs/handbook/2/template-literal-types.html). However, since the value of key of type symbol is not included in the return value of `Object.entries()` (see also #Appendix of this article), it is unnecessary and should be excluded. In fact, symbol types cannot be implicitly type-converted to string types(*3), and using them in Template Literal Types will result in a compile error.

- *3: [7.1.17 ToString ( argument )](https://tc39.es/ecma262/#sec-tostring)

```typescript
type SymbolStringLiteral<T extends symbol> = `${T}`
// TS2322: Type 'T' is not assignable to type 'string | number | bigint | boolean | null | undefined'.
```

Since the currently generated Entry type is a union type, once bound as `infer E`, only those Entries whose keys are of type `string | number` are extracted and converted using Template Literal Types.

The `undefined` in the result are excluded by `E extends [infer K, infer V]`, so `Exclude<>` is no longer necessary.

```typescript
type ObjectEntry<T extends {}> =
  T extends object ?
    { [K in keyof T]: [K, Required<T>[K]] }[keyof T] extends infer E ?
      E extends [infer K, infer V] ?
        K extends string | number ?
          [`${K}`, V] :
          never :
        never :
      never :
    never
```

In TypeScript 4.7, a new feature called ["`extends` Constraints on `infer` Type Variables"](https://devblogs.microsoft.com/typescript/announcing-typescript-4-7-beta/#extends-constraints-on-infer-type-variables) will be added, so that in the future this type definition can be written a little shorter.

```typescript
type ObjectEntry<T extends {}> =
  T extends object ?
    { [K in keyof T]: [K, Required<T>[K]] }[keyof T] extends infer E ?
      E extends [infer K extends string | number, infer V] ?
        [`${K}`, V] :
        never :
      never :
    never
```

...Oops, we forgot to consider the case where an array type is passed as a `typedEntries()` function argument. For a simple array type `Array<T>`, it is enough to return ``[`${number}`, T]``, however for Tuple, we would like to return a type like ``<I = 0 | 1 | 2 | ... > [`${I}`, T[I]]`` from type definition.

Fortunately, TypeScript allows for zero-based number generation and recursive type definition calls using Type-Level Programming, so type definitions can be defined as follows.

```typescript
type TupleEntry<T extends readonly unknown[], I extends unknown[] = [], R = never> =
  T extends readonly [infer Head, ...infer Tail] ?
    TupleEntry<Tail, [...I, unknown], R | [`${I['length']}`, Head]> :
    R

export type Entry<T extends {}> =
  T extends readonly [unknown, ...unknown[]] ?
    TupleEntry<T> :
    T extends ReadonlyArray<infer U> ?
      [`${number}`, U] :
      ObjectEntry<T>
```

The `I` of `TupleEntry<>` starts with an empty array, and each recursive call to the type definition increments the `length` property by adding one element to it. As a result, a zero-based number can be generated.

When using `ArrayLike<infer U>` to determine if an object is an array type or not, unintended type inference will occur with **object** like the following, so we need to use `ReadonlyArray<infer U>` to determine if it is an array type (*4).

- *4: `Array<T>` satisfies all the properties of `ReadonlyArray<T>`, so this type definition can determine these types together.

```typescript
const arrayLikeObject = {
  length: 2,
  1: "foo",
  2: "bar"
} as const;
type ArrayLikeObject = typeof arrayLikeObject

type ArrayEntry<T extends {}> = T extends ArrayLike<infer U> ? [`${number}`, U] : never

type E = ArrayEntry<ArrayLikeObject>
// [`${number}`, "foo" | "bar"]

Object.entries(arrayLikeObject).forEach(entry => { console.log(JSON.stringify(entry)); });
// [LOG]: "["1","foo"]" 
// [LOG]: "["2","bar"]" 
// [LOG]: "["length",2]" 
```

An empty array returns ``[`${number}`, never]``, however the result of `Object.entries([])` is also `[string, never][]`, so there is no special type definition for an empty array.

```typescript
type EmptyArrayEntry = Entry<[]>
// [`${number}`, never]

const entries = Object.entries([]);
// const entries: [string, never][]
```

If you have a special reason to return `never` for an empty array, you can use `type NonEmptyReadonlyArray<T> = readonly [T, ...T[]];` and then you need to change the type definition to something like `NonEmptyReadonlyArray<infer U>`.

This completes the type definitions for `Object.entries()`.

## Appendix

### Why did not change `Object.entries()` default behavior

You can change the behavior of `Object.entries()` itself by defining it as follows:

```typescript
declare global {
   interface ObjectConstructor {
     entries<T extends {}>(object: T): ReadonlyArray<Entry<T>>
  }
}
```

However, as mentioned above, objects obtained from processes with side effects or objects cast to types satisfying structural subtypes may not return the expected kay-value combination at runtime.

https://github.com/Microsoft/TypeScript/pull/12253#issuecomment-263132208

```typescript
const actual = {
  key: 'foo',
  extended: true
} as const;

type Base = { key: string }

const base: Base = actual;

Object.entries(base);
// Actually returns [['key', 'foo'], ['extended', true]]
```

Therefore, I am motivated to use it locally only when it is obvious that the expected kay-value combination will be returned, so I define a function that wraps `Object.entries()`.

### An interface does not extend `Record<>`

```typescript
interface IPerson {
  name: string;
}

type T = Extract<IPerson, Record<string | number | symbol, unknown>>
// never
```

#### Related TypeScript issues

- Types declared as an "interface" do not extend Record<PropertyKey, unknown> [#42825](https://github.com/microsoft/TypeScript/issues/42825)
- Types fulfill an interface, but interfaces do not [#41518](https://github.com/microsoft/TypeScript/issues/41518)
- Index signature is missing in type (only on interfaces, not on type alias) [#15300](https://github.com/microsoft/TypeScript/issues/15300)

> Just to fill people in, this behavior is currently by design. Because interfaces can be augmented by additional declarations but type aliases can't, it's "safer" (heavy quotes on that one) to infer an implicit index signature for type aliases than for interfaces. But we'll consider doing it for interfaces as well if that seems to make sense

<!-- -->

> Type aliases can implicitly fulfill an index signature (which is being declared at the Record<string> usage), but not interfaces (since they're subject to declaration merging)

### Object.entries() never returns the value of symbol type keys

Keys of symbol type are `non-String values`, so they are not enumerated in `EnumerableOwnPropertyNames` and are not included in the return value of `Object.entries()`

[20.1.2.5 Object.entries ( O )](https://tc39.es/ecma262/multipage/fundamental-objects.html#sec-object.entries)

> When the entries function is called with argument O, the following steps are taken:
> 
> 1. Let obj be ? `ToObject(O)`.
> 2. Let nameList be ? `EnumerableOwnPropertyNames(obj, key+value)`.
> 3. Return `CreateArrayFromList(nameList)`.

[7.3.24 EnumerableOwnPropertyNames ( O, kind )](https://tc39.es/ecma262/multipage/abstract-operations.html#sec-enumerableownpropertynames)

> It performs the following steps when called:
> 
> 1. Let ownKeys be ? `O.[[OwnPropertyKeys]]()`.
> 2. Let properties be a new empty List.
> 3. For each element key of ownKeys, do
>   * a. If `Type(key)` is String, then

[6.1.7 The Object Type](https://tc39.es/ecma262/#sec-object-type)

> A property key value is either an ECMAScript String value or a Symbol value. All String and Symbol values, including the empty String, are valid as property keys. A property name is a property key that is a String value.
> 
> An integer index is a String-valued property key that is a canonical numeric String (see 7.1.21) and whose numeric value is either +0𝔽 or a positive integral Number ≤ 𝔽(253 - 1).

[6.1.5 The Symbol Type](https://tc39.es/ecma262/#sec-ecmascript-language-types-symbol-type)

> The Symbol type is the set of all non-String values that may be used as the key of an Object property (6.1.7).

## Related links

- [Object.entries correct typings · Issue #35101 · microsoft/TypeScript](https://github.com/microsoft/TypeScript/issues/35101)

- [typescript - Create key/value type from object type - Stack Overflow](https://stackoverflow.com/a/69481624)

- [types - Typescript: keyof typeof union between object and primitive is always never - Stack Overflow](https://stackoverflow.com/a/52221718)

- [Typescript Key-Value relation preserving Object.entries type - Stack Overflow](https://stackoverflow.com/a/60142095)

### Articles for Type-Level Programming in TS

#### English

- [Generate literal type for numbers range in TypeScript](https://catchts.com/range-numbers)

#### Japanese

- [TypeScriptで型推論やりすぎな足し算をする関数を作ってみた - Qiita](https://qiita.com/shundroid/items/d970852456fd88d55f32)

- [TypeScriptの型レベルプログラミングで足し算・引き算・掛け算を実装する - asterisks](https://asterisks.netlify.app/2021/05/01/ts-tuple-calc/)

- [TypeScriptにヤバい機能が入りそうなのでひとしきり遊んでみる｜TechRacho by BPS株式会社](https://techracho.bpsinc.jp/yoshi/2020_09_04/97108)