11

When I use Object.fromEntries(entries) or Object.entires(obj) in typescript for my typed/const entries arrays or objt objects, I am losing the types to any or a broad type.

I can manually assign generic types (e.g. Record<string, number>) in some cases, but setting types of each pair/key is tedious.

Here is an example of what I want.

Typed Object.fromEntries(entries)

const myArrayOfPairs = [["a", 5], ["b", "hello"], ["c", false]] as const;

// The type of the following is "any"
const myTypelessObject = Object.fromEntries(myArrayOfPairs);

// I want the type of this one to be: { a: 5; b: "hello"; c: false; }
const myTypedObject = createTypedObjectFromEntries(myArrayOfPairs); 

Typed Object.entries(obj)

const myOldObject = {
    x: 6,
    y: "apple",
    z: true
};

// The type of the following is [string, string | number | boolean][]
const myEntries = Object.entries(myOldObject);

// I want the type of this one to be more specific.
// i.e.: (["x", number] | ["y", string] | ["z", boolean])[]
const myTypedEntries = getTypedObjectEntries(myOldObject);
Aidin
  • 25,146
  • 8
  • 76
  • 67

2 Answers2

12

Object.Entries(obj) -- Object to Array of KeyValue Pairs

This one is relatively straightforward. Using [K in keyof OBJ_T] you can get the keys, and OBJ_T[K] gives the relative value.

Here is a simple implementation of it:

// ~~~~~~~~~~~~~~~~~~~~~~~~ Utils ~~~~~~~~~~~~~~~~~~~~~~~~

type ObjectType = Record<PropertyKey, unknown>;
type PickByValue<OBJ_T, VALUE_T> // From https://stackoverflow.com/a/55153000
    = Pick<OBJ_T, { [K in keyof OBJ_T]: OBJ_T[K] extends VALUE_T ? K : never }[keyof OBJ_T]>;
type ObjectEntries<OBJ_T> // From https://stackoverflow.com/a/60142095
    = { [K in keyof OBJ_T]: [keyof PickByValue<OBJ_T, OBJ_T[K]>, OBJ_T[K]] }[keyof OBJ_T][];

// ~~~~~~~~~~~~~~~~~~~~ Typed Function ~~~~~~~~~~~~~~~~~~~~

function getTypedObjectEntries<OBJ_T extends ObjectType>(obj: OBJ_T): ObjectEntries<OBJ_T> {
    return Object.entries(obj) as ObjectEntries<OBJ_T>;
}

// ~~~~~~~~~~~~~~~~~~~~~~~~~ Test ~~~~~~~~~~~~~~~~~~~~~~~~~

const myOldObject = {
    x: 6,
    y: "apple",
    z: true
};

const myTypelessEntries = Object.entries(myOldObject); // type: [string, string | number | boolean][]

const myTypedEntries = getTypedObjectEntries(myOldObject);
type myTypedEntiresType = typeof myTypedEntries;  // type: (["x", number] | ["y", string] | ["z", boolean])[]

TS Playground. Note that you need es2019 to be the target in Configs.

Object.fromEntries(entries) -- Array of KeyValue Pairs to Object

This is one a bit more challenging.

You need to first extract the internal array pairs using infer, and then merge the result using the common UnionToIntersection utility type.

Here is the best I could come up with:

// ~~~~~~~~~~~~~~~~~~~~~~~~ Utils ~~~~~~~~~~~~~~~~~~~~~~~~

// Data Types
type EntriesType = [PropertyKey, unknown][] | ReadonlyArray<readonly [PropertyKey, unknown]>;

// Existing Utils
type DeepWritable<OBJ_T> = { -readonly [P in keyof OBJ_T]: DeepWritable<OBJ_T[P]> };
type UnionToIntersection<UNION_T> // From https://stackoverflow.com/a/50375286
    = (UNION_T extends any ? (k: UNION_T) => void : never) extends ((k: infer I) => void) ? I : never;

// New Utils
type UnionObjectFromArrayOfPairs<ARR_T extends EntriesType> =
    DeepWritable<ARR_T> extends (infer R)[] ? R extends [infer key, infer val] ? { [prop in key & PropertyKey]: val } : never : never;
type MergeIntersectingObjects<ObjT> = {[key in keyof ObjT]: ObjT[key]};
type EntriesToObject<ARR_T extends EntriesType> = MergeIntersectingObjects<UnionToIntersection<UnionObjectFromArrayOfPairs<ARR_T>>>;

// ~~~~~~~~~~~~~~~~~~~~~ Typed Functions ~~~~~~~~~~~~~~~~~~~~~

function createTypedObjectFromEntries<ARR_T extends EntriesType>(arr: ARR_T): EntriesToObject<ARR_T> {
    return Object.fromEntries(arr) as EntriesToObject<ARR_T>;
}

// ~~~~~~~~~~~~~~~~ Test for entries->object ~~~~~~~~~~~~~~~~~

const myArrayOfPairs = [["a", 5], ["b", "hello"], ["c", false]] as const;

const myTypelessObject = Object.fromEntries(myArrayOfPairs); // type: any

const myTypedObject = createTypedObjectFromEntries(myArrayOfPairs);
type myTypedObjectType = typeof myTypedObject; // type: { a: 5; b: "hello"; c: false; }

TS Playground

Aidin
  • 25,146
  • 8
  • 76
  • 67
  • 5
    This is awesome. How the heck one comes up with something like `type ObjectEntries`? Is there a tutorial or something? This is the darkest magic I've seen in a while and I can't really understand it, however the fact that it can't be done in an easy canonical way is quite weird to me. – PEZO May 28 '22 at 01:15
  • 1
    You could to all this with type-fest `Entries` : https://github.com/sindresorhus/type-fest . I highly recommend type-fest. – blake.vandercar Apr 27 '23 at 20:55
  • Also, you could simply modify the return type of `Object.entries` itself without creating the function `createTypedObjectFromEntries`: `interface ObjectConstructor { entries(o: T): Entries }` – blake.vandercar Apr 27 '23 at 20:55
  • Great solution! The only thing I couldn't really figure out was how `DeepWritable extends (infer R)[]` is distributive. Could you please elaborate a bit? – stratis Aug 20 '23 at 10:06
3

This is now easier to achieve on TypeScript 5 with the new const Type Parameter.

//
// Object.fromEntries
//

const typeSafeObjectFromEntries = <
  const T extends ReadonlyArray<readonly [PropertyKey, unknown]>
>(
  entries: T
): { [K in T[number] as K[0]]: K[1] } => {
  return Object.fromEntries(entries) as { [K in T[number] as K[0]]: K[1] };
};

const myObject = typeSafeObjectFromEntries([
  ["a", 5],
  ["b", "hello"],
  ["c", false],
]); // { a: 5; b: "hello"; c: false } ✅

//
// Object.entries
// (add const param for less broader types (ie. string -> "apple") -> const T extends Record<PropertyKey, unknown>)
//

const typeSafeObjectEntries = <T extends Record<PropertyKey, unknown>>(
  obj: T
): { [K in keyof T]: [K, T[K]] }[keyof T][] => {
  return Object.entries(obj) as { [K in keyof T]: [K, T[K]] }[keyof T][];
};

const myEntries = typeSafeObjectEntries({ x: 6, y: "apple", z: true });
// ["x", number] | ["y", string] | ["z", boolean])[] ✅
// with const param: (["x", 6] | ["y", "apple"] | ["z", true])[] ✅

TypeScript Playground

ajmnz
  • 742
  • 7
  • 19