はじめに
- JavaScript/TypeScript で
Array
をreduce()
を使って連想配列(JavaScriptの普通のオブジェクト)に畳み込む方法がある - 同様に Map オブジェクトに畳み込む方法もある
- そのうち忘れるはずなので、その時のためにメモを残しておく
何も考えず forEach で実装する
配列を処理/加工して、ハッシュマップや連想配列みたいなものにしたいことってあると思います。
前提として以下のような型やデータがあるとします。
interface Info {
name: string;
id: string;
// ...
}
type InfoMap = { [id: string]: Info };
const infos: Info[] = [
{ id: "1", name: "foo", /* ... */ },
{ id: "2", name: "bar", /* ... */ },
{ id: "3", name: "baz", /* ... */ },
];
ここで、infos
をもとに InfoMap
を作りたいとします。
何も考えないで実装すると、以下のようになると思います。
const infoMap: InfoMap = {};
infos.forEach((info) => {
infoMap[info.id] = info;
});
これでもいいんですけど、宣言的な感じじゃなくて、ちょっともやっとしますよね。
reduce で実装
JavaScript の Array
には reduce()
という、畳み込み用のメソッドがあります。
配列の合計を求めるとか、最大値を求めるとかに使ったりすると思います。
const data = [ 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 ];
const sum = data.reduce((a, b) => a + b, 0);
const maxData = data.reduce((a, b) => Math.max(a, b), Number.NEGATIVE_INFINITY);
上記の場合、配列要素と、畳み込み結果(および初期値)の型は同じ( number
)になっていますが、実は違っていても大丈夫らしいです。
なので、以下のようにして連想配列に畳み込むことができます。
const infoMap = infos.reduce<InfoMap>((map, info) => {
map[info.id] = info;
return map;
}, {});
ポイントは return map;
が必要なあたりでしょうか。
ちなみにジェネリック型を明示しないで、以下のように初期値を {} as InfoMap
とすることもできますが、やっぱり as
を使うよりは型安全な上記の方が良いのかな(初期値は空オブジェクトなのであまり変わらないかもしれないけど)。
const infoMap = infos.reduce((map, info) => {
map[info.id] = info;
return map;
}, {} as InfoMap);
最近の JavaScript には Map オブジェクトというものもあるので、Map オブジェクトに畳み込む例もメモしておきます。
const infoMap = infos.reduce(
(map, info) => map.set(info.id, info),
new Map<string, Info>()
);
Map オブジェクトの場合、set()
メソッドは、メソッドチェーンのために Map オブジェクトそのものを返すので、return map;
みたいな処理を書かなくてよいので少しすっきりしていますね。
ただし、単純な set()
以外のことがやりたい場合には、return map;
が必要になるので、注意が必要です。
もっと簡単な方法(追記)
単に、連想配列、Map オブジェクトにするだけであればもっと簡単な方法があります。
連想配列にする場合
const infoMap = Object.fromEntries(infos.map(x => [x.id, x]));
Map オブジェクトにする場合
const infoMap = new Map(infos.map(x => [x.id, x]));
ただ、もっと複雑な集計等をしたい(例:配列には月ごとのデータがあって、連想配列には同一キーごとに期間内の合計を求めたい)場合には reduce()
を使う必要があると思います。
(本記事は、そのあたりの説明が抜けているので注意してください。あとで見直すかも)
参考