【TypeScript】Object.entries()の返り値を厳格に型定義する

const obj = {
a: 1,
b: 'foo',
c: true,
};
Object.entries(obj); // => [string, string | number | boolean][]

Object.entries()の返り値でkeyが一律stringになってしまうため、返り値の型を厳格にしたい。

上記の例だと(["a", number] | ["b", string] | ["c", boolean])[]と型推論されるのが理想。

1. ユーティリティ型の実装

Mapped Typesを使う方法

type Obj = {
a: number;
b: string;
c: boolean;
};
type Entries<T extends Record<string, unknown>> = {
[K in keyof T]: [K, T[K]];
}[keyof T][];
type Result = Entries<Obj>; // => (["a", number] | ["b", string] | ["c", boolean])[]

Mapped Typesでkeyとvalueを関連付ける方法1。今回の中だと一番シンプルな気がする。

Distributive Conditional Typesを使う方法

type Entries<
T extends Record<string, unknown>,
K extends keyof T = keyof T,
> = (K extends any ? [K, T[K]] : never)[];
type Result = Entries<Obj>; // => (["a", number] | ["b", string] | ["c", boolean])[]

Distributive Conditional TypesでUnion型のkeyを分散させ、keyとvalueを関連付ける方法。

inferを使う方法

type Entries<T extends Record<string, unknown>> = (keyof T extends infer U
? U extends keyof T
? [U, T[U]]
: never
: never)[];
type Result = Entries<Obj>; // => (["a", number] | ["b", string] | ["c", boolean])[]

タプルにする方法

type Entries<T extends Record<string, unknown>> = [keyof T, T[keyof T]][];
type Result = Entries<Obj>; // => [keyof Obj, string | number | boolean][]

他と比べればシンプルだが、keyとvalueの両方がUnion型なタプルとなってしまうので厳格さは欠けてしまう。

2. ラッパー関数の実装

function objectEntries<T extends Record<string, unknown>>(obj: T): Entries<T> {
return Object.entries(obj) as Entries<T>;
}
objectEntries(obj); // => (["a", number] | ["b", string] | ["c", boolean])[]

23

参考
  1. keyがnumberの場合stringに変換されてしまうため、keyがstringなオブジェクトのみを受け付けるようにしている

  2. Typescript Key-Value relation preserving Object.entries type

  3. Object.entriesの戻り値の型を厳密にする