Object.entriesの戻り値の型を厳密にする
やりたいこと
const entries = Object.entries({
a: 123,
b: 'abc',
c: true,
})
上記のように定義したentries
の型は[string, number | string | boolean][]
と推論されます。
この推論では不都合があるので['a', number] | ['b', string] | ['c', boolean]
のような型に変換したい、というのが当記事の主題です。
Object.entries
の戻り値の型にはどのような不都合があるのか?
例えば、下記のような場合はどうでしょうか?
const strKeys = ['a', 'b', 'c'] as const
const numKeys = ['d', 'e', 'f'] as const
type StrKeys = typeof strKeys[number]
type NumKeys = typeof numKeys[number]
type Obj = {
[Key in StrKeys | NumKeys]: Key extends StrKeys ? string : number
}
const obj: Obj = {
a: 'a', b: 'b', c: 'c',
d: 123, e: 456, f: 789,
}
for (const [key, value] of Object.entries(obj)) {
if (strKeys.includes(key)) { // ①-1
// ②-1
}
if (numKeys.includes(key)) { // ①-2
// ②-2
}
}
マップ型で定義したプロパティ名によって、プロパティ値の扱いを変えたいような例です。
ここで2点問題が発生しました。
-
key
の型はstring
として推論されるため、①の行で型エラーが発生する。 -
value
の型はobj
に設定された全プロパティ値の型のユニオンになるため、②の行で制御フロー分析が効かない。
このままでは本来やりたいことがエラーに阻まれてしまってできません。
では、なぜこのような問題が発生するのでしょうか?
Object.entries
の戻り値の型定義
TypeScriptの標準ライブラリで定義されているentries
の型を見てみましょう。
/*! *****************************************************************************
Copyright (c) Microsoft Corporation. All rights reserved.
Licensed under the Apache License, Version 2.0 (the "License"); you may not use
this file except in compliance with the License. You may obtain a copy of the
License at http://www.apache.org/licenses/LICENSE-2.0
THIS CODE IS PROVIDED ON AN *AS IS* BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
KIND, EITHER EXPRESS OR IMPLIED, INCLUDING WITHOUT LIMITATION ANY IMPLIED
WARRANTIES OR CONDITIONS OF TITLE, FITNESS FOR A PARTICULAR PURPOSE,
MERCHANTABLITY OR NON-INFRINGEMENT.
See the Apache Version 2.0 License for specific language governing permissions
and limitations under the License.
***************************************************************************** */
entries<T>(o: { [s: string]: T } | ArrayLike<T>): [string, T][];
Object.entries
が返却する配列の要素はすべて[string, T]
と定義されています。
つまりプロパティの名前と値の型の組み合わせ情報は破棄されていて、プロパティ名によるプロパティ値の型の推論は効かなくなっています。
Object.entries
の戻り値の型を厳密にする
ここからが本題。Object.entries
の戻り値の型をより厳密に定義していきます。
ゴールは下記のような汎用型Entries<T>
を定義することです。
type Obj = { // 任意の型のプロパティを持つオブジェクト型
a: string
b: number
c: boolean
}
type Result = Entries<Obj>
// Result = (['a', string] | ['b', number] | ['c', boolean])[]
上記の結果を得ようとして単純に下記のように定義してしまうと、プロパティ名のユニオンとプロパティ値の型のユニオンを要素として持つタプルの配列となってしまい、目的の型は得られません。
type NotGood<T> = [keyof T, T[keyof T]][]
type Result = NotGood<Obj>
// Result = ['a' | 'b' | 'c', string | number | boolean][]
これは、keyof T
とT[keyof T]
が独立して定義されているために発生する事象です。
マップ型のようにin
を使ってタプルを定義できれば楽なんですが、タプルの性質上できません。
では、keyof T
とT[keyof T]
を関連付けて定義しましょう。
type Entries<T> = (keyof T extends infer U
? U extends keyof T
? [U, T[U]]
: never
: never)[]
type Result = Entries<Obj>
// Result = (['a', string] | ['b', number] | ['c', boolean])[]
これで目的の結果を得ることができました。
後はObject.entries
をラッピングした関数を定義すれば型が扱いやすくなります。
(必然的にas
を使わないと行けないので若干ダサいですが……)
function getEntries<T extends Record<string, unknown>>(obj: T): Entries<T> {
return Object.entries(obj) as Entries<T>
}
結論
Object.entries
の戻り値の型を厳密にするには下記のような型を定義する。
type Entries<T> = (keyof T extends infer U
? U extends keyof T
? [U, T[U]]
: never
: never)[]
Discussion
type-festのEntriesを使って少しやってみました。
定義側
使用側
demo code.
簡単ですが、以上です。