JavaScript Tips | 配列ユーティリティ:フラット化

JavaScript JavaScript
スポンサーリンク

何をしたいユーティリティか:「配列のフラット化」

ここでの「フラット化」は、ネスト(入れ子)になっている配列を、浅い配列に“平らにする”処理のことです。
英語だと flattenflat と呼ばれます。

例えば、こういう配列があります。

const nested = [1, [2, 3], [4, [5, 6]]];
JavaScript

これを

[1, 2, 3, 4, 5, 6]
JavaScript

のように、「中の配列をほどいて 1 本の配列」にしたい――これがフラット化です。
業務では、「API から配列の配列が返ってくる」「グループごとの結果を最後に 1 本にまとめたい」といった場面でよく使います。


一番基本:Array.prototype.flat の使い方

flat の基本形

最近の JavaScript には、標準で Array.prototype.flat があります。
これは「指定した深さまで配列をフラット化する」メソッドです。

const nested = [1, [2, 3], [4, [5, 6]]];

nested.flat();
// デフォルトは depth = 1
// [1, 2, 3, 4, [5, 6]]
JavaScript

flat() だけだと「1 段だけ」フラット化されます。
もっと深くまでフラット化したいときは、引数に「深さ」を指定します。

nested.flat(2);
// [1, 2, 3, 4, 5, 6]
JavaScript

flat(2) は、「最大 2 段までネストをほどく」という意味です。

完全にフラットにしたいとき

ネストの深さがよく分からない、でも「とにかく全部フラットにしたい」というときは、
Infinity を渡すのが定番です。

nested.flat(Infinity);
// [1, 2, 3, 4, 5, 6]
JavaScript

「どこまでネストしていても、全部ほどく」という意味になります。


実務でよくあるフラット化のパターン

例1:配列の配列を 1 本にまとめる

例えば、ページングされた結果や、グループごとの結果を配列の配列で持っているとします。

const pages = [
  [{ id: 1 }, { id: 2 }],
  [{ id: 3 }, { id: 4 }],
  [{ id: 5 }],
];
JavaScript

これを「全件 1 本の配列」にしたいとき、flat がそのまま使えます。

const all = pages.flat();
// [{ id: 1 }, { id: 2 }, { id: 3 }, { id: 4 }, { id: 5 }]
JavaScript

「1 段だけネストしている」典型的なケースです。

例2:グループ化した結果をフラットに戻す

以前やった「グループ化ユーティリティ」で、
「カテゴリごとに配列に分けた」あと、「やっぱり全部 1 本に戻したい」こともあります。

const grouped = {
  A: [{ id: 1 }, { id: 2 }],
  B: [{ id: 3 }],
};

const values = Object.values(grouped);
// [[{ id: 1 }, { id: 2 }], [{ id: 3 }]]

const all = values.flat();
// [{ id: 1 }, { id: 2 }, { id: 3 }]
JavaScript

「オブジェクトの値(配列)を取り出して、それを flat する」というパターンは、集計系でよく出てきます。


flat が使えない環境向け:自前のフラット化ユーティリティ

1 段だけフラットにする flattenOnce

古い環境や、ポリフィルを入れたくない場合のために、自前のユーティリティも書いておきます。
まずは「1 段だけ」フラットにする版です。

function flattenOnce(array) {
  if (!Array.isArray(array)) {
    return [];
  }

  const result = [];

  for (const item of array) {
    if (Array.isArray(item)) {
      result.push(...item);
    } else {
      result.push(item);
    }
  }

  return result;
}
JavaScript

やっていることはシンプルで、「要素が配列なら中身を展開して push、そうでなければそのまま push」です。
これは array.flat()(depth 1)と同じ動きです。

任意の深さまでフラットにする flattenDepth

次に、「指定した深さまでフラットにする」版です。

function flattenDepth(array, depth = 1) {
  if (!Array.isArray(array)) {
    return [];
  }

  if (depth <= 0) {
    return array.slice();
  }

  const result = [];

  for (const item of array) {
    if (Array.isArray(item)) {
      const flatItem = flattenDepth(item, depth - 1);
      result.push(...flatItem);
    } else {
      result.push(item);
    }
  }

  return result;
}
JavaScript

ここが重要ポイントです。

depth が 0 以下になったら、それ以上はほどかずにそのまま返す。
配列を見つけたら、depth - 1 で再帰的に flattenDepth を呼ぶ。
そうでない要素はそのまま push。

これで、「最大 depth 段までネストをほどく」動きになります。

完全にフラットにする flattenAll

「とにかく全部フラットにしたい」版も書いておきます。

function flattenAll(array) {
  if (!Array.isArray(array)) {
    return [];
  }

  const result = [];

  for (const item of array) {
    if (Array.isArray(item)) {
      const flatItem = flattenAll(item);
      result.push(...flatItem);
    } else {
      result.push(item);
    }
  }

  return result;
}
JavaScript

flattenDepth の depth 無限版だと思ってください。
配列を見つけたら、ひたすら再帰的にほどいていきます。


フラット化と map を組み合わせる:flatMap 的なユーティリティ

「map した結果が配列」なときのあるある

業務では、「1 要素から複数の要素を返す map」を書くことがよくあります。
例えば、「1 つの注文から複数の明細行を取り出す」ようなケースです。

const orders = [
  { id: 1, items: [/* 明細配列 */] },
  { id: 2, items: [/* 明細配列 */] },
];
JavaScript

これを「全明細行の配列」にしたいとき、素直に書くとこうなります。

const mapped = orders.map((order) => order.items);
// [[...], [...]]

const allItems = mapped.flat();
// [...]
JavaScript

「map → flat」というセットが毎回出てくるので、
これを 1 つにまとめた flatMap 的なユーティリティを用意しておくと便利です。

flatMap ユーティリティ

function flatMap(array, mapper) {
  if (!Array.isArray(array)) {
    return [];
  }

  const result = [];

  for (let i = 0; i < array.length; i++) {
    const mapped = mapper(array[i], i, array);

    if (Array.isArray(mapped)) {
      result.push(...mapped);
    } else if (mapped != null) {
      result.push(mapped);
    }
  }

  return result;
}
JavaScript

mapper は、「要素を受け取って、配列または単一要素を返す関数」です。
返り値が配列なら展開して push、単一要素ならそのまま push、null/undefined は無視しています。

実際の使い方

const orders = [
  { id: 1, items: [{ sku: "A" }, { sku: "B" }] },
  { id: 2, items: [{ sku: "C" }] },
];

const allItems = flatMap(orders, (order) => order.items);
// [{ sku: "A" }, { sku: "B" }, { sku: "C" }]
JavaScript

「map してから flat する」というパターンを、1 つの関数に閉じ込めたイメージです。


実務で意識してほしい設計のポイント

「どこまでフラットにするか」を明示する

フラット化は強力ですが、「どこまでほどくか」を決めないと危険です。
特に、構造を完全に失ってしまうと、「どの要素がどこから来たのか」が分からなくなります。

例えば、「ページごとの結果」を完全にフラットにしてしまうと、「どのページに属していたか」が消えます。
それが問題になる場合は、「1 段だけフラット」「2 段だけフラット」のように、depth を意識して使うべきです。

「配列かどうか」をちゃんと判定する

フラット化ユーティリティでは、「配列かどうか」を Array.isArray で判定するのが基本です。
typeof value === "object" だと、オブジェクトまでほどいてしまう危険があります。

「配列だけをほどく」「オブジェクトはそのまま」という線引きを、ユーティリティの中でしっかり決めておくと、予期せぬバグを防げます。

「フラット化」と「結合」の違いを意識する

結合(merge)は、「複数の配列を横に並べて 1 本にする」イメージです。
フラット化(flatten)は、「配列の中にある配列をほどいて 1 本にする」イメージです。

配列の配列を扱うとき、「これは結合したいのか」「構造をほどきたいのか」を意識して、
merge 系ユーティリティと flatten 系ユーティリティを使い分けると、コードの意図がとてもクリアになります。


少し手を動かして感覚をつかむ

コンソールで、次のようなコードを実際に打ってみてください。

const nested = [1, [2, 3], [4, [5, 6]]];

nested.flat();
nested.flat(2);
nested.flat(Infinity);

flattenOnce(nested);
flattenDepth(nested, 1);
flattenDepth(nested, 2);
flattenAll(nested);

const pages = [
  [{ id: 1 }, { id: 2 }],
  [{ id: 3 }],
];

pages.flat();

const orders = [
  { id: 1, items: [{ sku: "A" }, { sku: "B" }] },
  { id: 2, items: [{ sku: "C" }] },
];

flatMap(orders, (order) => order.items);
JavaScript

「どの程度フラットになっているか」「構造がどこまで残っているか」「flat と自前ユーティリティの挙動の違い」を、自分の目で確認してみてください。

そのうえで、自分のプロジェクトに

export function flattenOnce(...) { ... }
export function flattenDepth(...) { ... }
export function flattenAll(...) { ... }
export function flatMap(...) { ... }
JavaScript

のような関数を置き、

「ネストした配列をほどきたくなったら、必ずこの“フラット化ユーティリティ”を通す」

というルールを作ってみてください。
そうすると、あなたの配列処理は、「場当たり的なネスト処理」から、「意図がはっきりしたデータ変換」に一段ステップアップしていきます。

タイトルとURLをコピーしました