DEV Community

Cover image for How to add types for Object.fromEntries
Jakub Švehla
Jakub Švehla

Posted on • Edited on

How to add types for Object.fromEntries

Step by step tutorial on how to create a proper type for Object.fromEntries() which can work with tuples and read-only data structures.

TLDR:

Source code for Object.fromEntries type generic is at bottom of the article.
You can copy-paste it into your IDE and play with it.

VS-code preview




const data = [
  ['key1', 'value1' as string],
  ['key2', 3]
]  as const

const result = Object.fromEntries(data)


Enter fullscreen mode Exit fullscreen mode

Alt Text

Motivation

The default typescript type for Object.fromEntries definition looks like this



interface ObjectConstructor {
  // ...
  fromEntries(entries: Iterable<readonly any[]>): any;
}


Enter fullscreen mode Exit fullscreen mode

As you can see the usage of return value : any it's not the best one. So we will redeclare static types for this method via the usage of the strongest Typescript tools which are described below.

Prerequisite

before we will continue we have to know the typescript keyword infer and some basic generics usage.
https://www.typescriptlang.org/docs/handbook/release-notes/typescript-2-8.html#type-inference-in-conditional-types

If you want to deep dive into advanced typescript types I recommend this typescript series full of useful examples.


Let's start hacking

First of all, we will define Cast<X, Y> generic which helps us to build our target FromEntries<T> type.

Cast<X, Y>

This generic help us to bypass the typescript compiler for passing invalid types. We will use Cast<X, Y> to "shrink" a union type to the other type which is defined as the second parameter.



type Cast<X, Y> = X extends Y ? X : Y


Enter fullscreen mode Exit fullscreen mode

Preview



type T4 = string | number
type T5 = Cast<T4, string>


Enter fullscreen mode Exit fullscreen mode

Alt Text

Okay... it should be enough for this moment. We can start with the FromEntries<T> generic.

FromEntries<T>

So let's define a new type FromEntriesV1<T>. It takes one argument T and checks if the argument is a two-dimensional matrix [any, any][] if yes, create proper type. if no return default behavior which just returns unknown untyped Object { [key in string]: any }.



type FromEntriesV1<T> = T extends [infer Key, any][]
  // Cast<X, Y> ensure TS Compiler Key to be of type `string`
  ? { [K in Cast<Key, string>]: any }
  : { [key in string]: any } 


Enter fullscreen mode Exit fullscreen mode


type ResFromEV1 = FromEntriesV1<[
  ['key1', 'value1'],
  ['key2', 3],
]>


Enter fullscreen mode Exit fullscreen mode

Alt Text

It works the same even without Cast<Key, string> generic but Typescript compiler still warning you that there is a potential error so we have to bypass it with the Cast<X, Y>

This generic works thanks to infer which extracts out all keys into a union type which is used as target object keys.

Now we have to set the correct values of the object but before we will do it let's introduce another generics ArrayElement<A>.

ArrayElement<A>

this simple generic helps us to extract data outside of an Array<T> wrapper.



export type ArrayElement<A> = A extends readonly (infer T)[]
  ? T
  : never


Enter fullscreen mode Exit fullscreen mode

Preview



type T1 = ArrayElement<['foo', 'bar']>


Enter fullscreen mode Exit fullscreen mode


const data = ['foo', 'bar'] as const
type Data = typeof data
type T2 = ArrayElement<Data>


Enter fullscreen mode Exit fullscreen mode

Alt Text

Okey we can continue with adding proper value into the new object. We just simply set that value is second item of nested tuple ArrayElement<T>[1].



type FromEntriesV2<T> = T extends [infer Key, any][]
  ? { [K in Cast<Key, string>]: ArrayElement<T>[1] }
  : { [key in string]: any }


Enter fullscreen mode Exit fullscreen mode

Alt Text

we successfully extracted all possible values but as we can see, there is a missing connection between key and value in our new type.

If we want to fix it we have to know another generic Extract<T>. Extract<T> is included in the official standard typescript library called utility-types.

This generic is defined as:



type Extract<T, U> = T extends U ? T : never;


Enter fullscreen mode Exit fullscreen mode

official documentation: https://www.typescriptlang.org/docs/handbook/utility-types.html#extracttype-union

Thanks to this generic we can create connections between keys and values of nested tuples



type FromEntries<T> = T extends [infer Key, any][]
  ? { [K in Cast<Key, string>]: Extract<ArrayElement<T>, [K, any]>[1] }
  : { [key in string]: any }


Enter fullscreen mode Exit fullscreen mode

Preview



type Result = FromEntries<[
  ['key1', 'value1'],
  ['key2', 3],
]>


Enter fullscreen mode Exit fullscreen mode

Alt Text

And... that's all!!! Good job! we did it 🎉 now the generics can transfer an Array of tuples into object type.


Oh, wait. there is still some major issues which we should solve

Generic does not work well with readonly Notations like in the example below



const data = [['key1', 1], ['key2', 2]] as const
type Data = typeof data
type Res = FromEntries<Data>


Enter fullscreen mode Exit fullscreen mode

Alt Text

Alt Text

To resolve this issue let's introduce another generic DeepWriteable

DeepWriteable<T>

this generic is used to recursively remove all readonly notations from the data type.
If you create type by typeof (data as const) all keys start with the readonly prefix so we need to remove it to make all objects consistent.



type DeepWriteable<T> = { -readonly [P in keyof T]: DeepWriteable<T[P]> }


Enter fullscreen mode Exit fullscreen mode

Preview



const data = ['foo', 'bar'] as const
type Data = typeof data
type T3 = DeepWriteable<Data>


Enter fullscreen mode Exit fullscreen mode

Alt Text

With this new knowledge, we can fix unexpected behavior and make it all works again.



const data = [['key1', 1], ['key2', 2]] as const
type Data = typeof data

type T6 = FromEntries<DeepWriteable<Data>>


Enter fullscreen mode Exit fullscreen mode

Alt Text

Final source code + Redeclare global Object behavior

If you don't know what declare {module} annotations in typescript is, You can check official documentation https://www.typescriptlang.org/docs/handbook/modules.html

We will use this feature to redeclare the global type behavior of Object.fromEntries.

All you need to do is just paste the code below to your index.d.ts or global.d.ts.




export type ArrayElement<A> = A extends readonly (infer T)[] ? T : never;
type DeepWriteable<T> = { -readonly [P in keyof T]: DeepWriteable<T[P]> };
type Cast<X, Y> = X extends Y ? X : Y
type FromEntries<T> = T extends [infer Key, any][]
  ? { [K in Cast<Key, string>]: Extract<ArrayElement<T>, [K, any]>[1]}
  : { [key in string]: any }

export type FromEntriesWithReadOnly<T> = FromEntries<DeepWriteable<T>>


declare global {
   interface ObjectConstructor {
     fromEntries<T>(obj: T): FromEntriesWithReadOnly<T>
  }
}


Enter fullscreen mode Exit fullscreen mode

And voilá 🎉 🎉 🎉 🎉 🎉 🎉
We are done

I hope that you enjoyed this article the same as me and learned something new. If yes don't forget to like this article

Top comments (8)

Collapse
 
benlesh profile image
Ben Lesh

This is a GREAT article! Near miss in one small spot: Cast should be used to cast to PropertyKey instead of string. Then number and symbol will work as well!

Example here:

typescriptlang.org/play?#code/PTAE...

Collapse
 
svehla profile image
Jakub Švehla

wow, very nice! 😎 thanks for sharing

Collapse
 
mindplay profile image
Rasmus Schultz

Why isn't this type just built-in for Object.fromEntries? 🤨

You should consider making a pull-request to add these types.

Collapse
 
dwelle profile image
David Luzar • Edited

Good article. Built-in generics, especially DOM-related, sadly aren't very good.

Btw, you can replace the ArrayElement<T> generic with a simple T[number].

Collapse
 
svehla profile image
Jakub Švehla

Hah, now I'm using T[number] 100% of the time. Thanks for the hint!

Collapse
 
fordcarta profile image
ford-carta

Hey this is super cool! I have two question if you don't mind.

  1. Do you know how to go from a {key1: 1, key2: 2} as const to [['key1', 1], ['key2', 2]] as const. It seems like this is impossible with Object.entries since the types are lost.
  2. Do you know how we could adapt the solution you gave for arbitrarily nested arrays. For example [['key1', 1], ['key2', ['key2_1',2.1]]] as const
Collapse
 
wtho profile image
wtho

Wow, awesome!

How about a follow-up with refining Object.entries types so that Object.fromEntries(Object.entries(...)) retains the same type?

Collapse
 
illia_chaban profile image
Illia Chaban

there you go

type Entry<T extends Record<string, unknown>> = {
  [K in keyof T]: [K, T[K]];
}[keyof T];

export const entries =  Object.entries as <const T extends Record<string,unknown>>(
  obj: T
) => Entry<T>[] 
Enter fullscreen mode Exit fullscreen mode