🥿

[JS / TS]flatMapっていつ使うん?

2022/12/17に公開1

はじめに

コードレビューをしてもらった時に、flatMap()ってこういう時に使えるのか!
っとなった時のメモ

flatMapとは?

flatMap メソッドは、map の後に深さ 1 の flat を呼び出すのと同じです。

https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Global_Objects/Array/flatMap

flatMap() は上記の通り、map()flat()を組み合わせたメソッドです。
それぞれ軽く見てみると、

  • map()
    • 各要素に対して一度ずつ呼び出して処理
    • 処理結果を新しい配列として返却
  • flat()
    • 配列の要素を指定した深さ(ネスト)で結合
    • 結合した新しい配列を返却。

むむ?
map()はさておき、flat() がわかりづらい、、
実装例を見てみると

const arr1 = [0, 1, 2, [3, 4]];

console.log(arr1.flat());
// expected output: [0, 1, 2, 3, 4]

のように、配列のネスト具合を浅くするメソッドのようです。

つまり、上記をまとめると

  • 配列の各要素に対して処理して
  • 処理結果のネストを浅くした配列を返す

のがflatMap()ということになります。

配列のネストを浅くする、、?
使い所がパッとイメージできないです、、

こんな時に使えるかも!

前述の通り、(私の場合は)flatMap()の使い所がイマイチ掴めず、後回しの対象になってました。
が、レビューで頂いた実装を見て、
「flatMapさんバカにしてすみません、、」
となりました。

要素の抽出

flatMap()さんは、配列やオブジェクトの要素を抽出する時に、非常に便利に使えるメソッドです。
下記の例では、stringまたはnumber型の配列から、number型の値のみ抽出しています。

type HogeType = (string | number)[];

const hoge: HogeType = ["aaa", "bbb", 111, 222];

const mapResult = hoge.map((v) => (typeof v === "number" ? v : [])); // [[], [], 111, 222]
const flatMapResult = hoge.flatMap((v) => (typeof v === "number" ? v : [])); // [111, 222]

オブジェクトの場合は下記の感じです。

type HogeType = {
  a: string;
  b: string;
  c: number;
  d: number;
};
const hoge: HogeType = { a: "aaa", b: "bbb", c: 111, d: 222 };

const mapResult = Object.values(hoge).map((v) => (typeof v === "number" ? v : [])); // [[], [], 111, 222]
const flatMapResut = Object.values(hoge).flatMap((v) => (typeof v === "number" ? v : [])); // [111, 222]

ポイントは、条件に当てはまらない場合は[]を返すところです。
これにより、.map()[[], [], 111, 222] となったものが、
.flat()の処理で空配列が削除され[111, 222]が抽出される仕組みです。

まとめ

  • .flatMap().map() + .flat()した処理である
  • .flatMap()は、条件に当てはまらない場合は[]を返すことで、値の抽出ができる

具体的に「こういう場面で使える!」という選択肢が増えると、より面白さを感じられますね。
これだからプログラミングはやめられないってばよ。。

※他にもこんな使い方ができるよってものがありましたら、コメント頂けると幸いです。

参考

https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Global_Objects/Array/flatMap
https://qiita.com/NeGI1009/items/4befe4695a4712d96d56
https://zenn.dev/yarnaimo/articles/66e9102a79c0896b4204

Discussion

nap5nap5

※他にもこんな使い方ができるよってものがありましたら、コメント頂けると幸いです。

zodのsafeParseとのコラボは便利かもです。

demo code.
https://codesandbox.io/p/sandbox/confident-maria-rd8wfw?file=/src/index.ts:1,1

test("", () => {
  const data: UserPartialData[] = [
    {
      id: "1",
    },
    undefined,
    {
      name: "Benny"
    },
    {
      id: "3",
      name: "Mike"
    },
    null
  ]
  const users = data.flatMap(d => {
    if (d == null) return []
    const parsed = UserSchema.safeParse(d)
    if (parsed.success) return [parsed.data]
    return []
  })
  expect(users).toStrictEqual([
    {
      id: "3",
      name: "Mike"
    },
  ])
})

mergeMaps関数を定義すると便利かもです。Flutterにはデフォで提供してくれているみたいです

demo code.

https://codesandbox.io/p/sandbox/stupefied-hugle-44myld?file=/src/index.ts:16,1

// @see https://github.com/google/grr/blob/master/grr/server/grr_response_server/gui/ui/lib/type_utils.ts#L67-L72
export const mergeMaps = <K, V>(
  ...maps: ReadonlyArray<ReadonlyMap<K, V> | Map<K, V> | null | undefined>):
  ReadonlyMap<K, V> => {
  return new Map(maps.flatMap(map => map ? [...map] : []));
}

test("", () => {
  const map1 = new Map<string, number>()
  map1.set("a", 1)
  map1.set("b", 2)

  const map2 = new Map<string, number>()
  map2.set("b", 3)
  map2.set("c", 4)

  const map3 = new Map<string, number>()
  map2.set("d", 5)
  map2.set("e", 6)

  const mergedMap = mergeMaps(map1, map2, map3)
  expect(mergedMap).toStrictEqual(new Map<string, number>([
    ["a", 1],
    ["b", 3],
    ["c", 4],
    ["d", 5],
    ["e", 6],
  ]))
})

簡単ですが、以上です。