🤔
型安全にオブジェクトの値をmapしたい(Object.entriesとObject.fromEntriesで)
Object.entriesとObject.fromEntriesを使ってオブジェクトの値を変換できます。
例えばこんな感じのやつです。
const mapObjectValue = (data, mapfn) => {
const keyValueList = Object.entries(data)
const mappedList = keyValueList.map(([key,value]) => [
key,
typeof value === "object" && value != null && value.constructor.name === "Object"
? mapObjectValue(value, mapfn)
: mapfn(value)
])
return Object.fromEntries(mappedList)
}
再帰関数でネストしたオブジェクトに対応しています。
クラスを展開してもしょうがないので、クラス(constructor.name
が"Object"
以外のオブジェクト)にはmapFn
の処理を適用させています。
Dateを展開しても空のオブジェクトになるだけですし。
出力サンプル
const data = {
num: 1,
str: "one",
date: new Date(),
obj: {
num: 2,
str: "two",
obj: {
num: 3,
str: "three",
}
}
};
// オブジェクトの値をすべてstringに変換する
console.log(mapObjectValue(data, value => String(value)))
/**
* output
{
"num": "1",
"str": "one",
"date": "Sat Feb 11 2023 23:33:41 GMT+0900 (日本標準時)",
"obj": {
"num": "2",
"str": "two",
"obj": {
"num": "3",
"str": "three"
}
}
}
*/
TypeScript化する
エラーが出ない程度のパターン
申し訳程度に型を設定すると返り値で型情報が失われてしまいます。
const mapObjectValue = <T extends Object, R>(data: T, mapfn: (value: any) => R) => {
const keyValueList = Object.entries(data)
const mappedList = keyValueList.map(([key,value]) => [
key,
typeof value === "object" && value != null
? mapObjectValue(value, mapfn)
: mapfn(value)
]) as [keyof T, any][]
return Object.fromEntries(mappedList)
}
const ret = mapObjectValue(data, (value) => String(value))
/**
* 推論された型
const ret: {
[k: string]: any;
}
*/
返り値の型を指定するパターン
返り値の型を再帰で定義してみます。
// Tの値をすべてR型に変更する型
type ValueMapped<T,R> = { [K in keyof T]: T[K] extends object ? ValueMapped<T[K], R>: R}
const mapObjectValue = <T extends object,R> (data: T, mapfn: (_: any) => R): ValueMapped<T,R> => {
const keyValueList = Object.entries(data)
const mappedList = keyValueList.map(([key,value]) => [
key,
typeof value === "object" && value != null && value.constructor.name === "Object"
? mapObjectValue(value, mapfn)
: mapfn(value)
])
return Object.fromEntries(mappedList)
}
const ret = mapObjectValue(data, (value) => String(value))
/**
* 推論された型
{
num: string,
str: string,
date: object, ☆ 実態はstring
obj: {
num: string,
str: string,
obj: {
num: string,
str: string
}
}
}
*/
なんとなくうまくいってそうに見えますが失敗しています。
問題点
- classもobjectとして扱われている
- TypeScriptではclassとMap(keyとvalueの集合)を区別できない
- 自分の書くコードはclassを作らない手段を取れますが、クラスベースで作られたライブラリや
Date
やRexExp
など組み込みclassがある
-
mapFn
の引数の型がany
になっているので必要のない型チェックをする必要がある- ネストしたオブジェクトの値の型を一つにまとめたUnionTypesを作ることができれば可能だがTypeScriptでは不可能(なハズ)
まとめ
というわけでできると思ってやってみたらできなかった話でした。
「実はできるよ」と誰かがコメントしてくれることを祈っています。
余談ですが4.9で追加されたsatisfies
で型チェックができて便利ですね。
type ExpectedType = {
num: string,
str: string,
date: string,
obj: {
num: string,
str: string,
obj: {
num: string,
str: string
}
}
}
const ret = mapObjectValue(data, (value) => String(value)) satisfies ExpectedType // 型不一致でエラーが出る
Discussion
達成したいことがやや不透明ですが、汲み取って少しやってみました。
定義側
使用側
demo code.
簡単ですが、以上です。