TypeScriptにおいて、多次元配列を正しく扱うことは、複雑なデータ構造を安全に管理するための第一歩です。
アプリケーションの開発では、スプレッドシートのようなグリッドデータや、ゲームのマップ情報、あるいは統計データの行列など、多層的な配列を扱う機会が非常に多くあります。
JavaScriptでは配列の中に任意の要素を入れられますが、TypeScriptを導入することで「何次元目にどのようなデータが入っているか」を厳格に定義できます。
本記事では、初心者から中級者の方に向けて、多次元配列の基本的な型定義から、開発現場で役立つ実践的な操作テクニックまでを詳しく解説します。
多次元配列の基本的な型定義
TypeScriptで多次元配列を定義する方法には、主に2つの構文が存在します。
プロジェクトのコーディング規約や、データの複雑さに合わせて適切なものを選択することが重要です。
角括弧(ブラケット)を用いた定義
最も一般的で直感的な方法は、型の後ろに [] を重ねて記述するスタイルです。
例えば、数値型の要素を持つ2次元配列を定義する場合、number[][] と記述します。
// 数値型の2次元配列を定義
const matrix: number[][] = [
[1, 2, 3],
[4, 5, 6],
[7, 8, 9]
];
// 3次元配列の場合はさらに[]を追加
const cube: number[][][] = [
[[1, 2], [3, 4]],
[[5, 6], [7, 8]]
];
この記述法は非常にシンプルであり、次元数が増えるほど [] を追加するだけなので、視覚的に構造を把握しやすいというメリットがあります。
一方で、4次元や5次元といった深いネストになると、一目で構造を理解するのが難しくなる場合もあります。
Generic(Array<T>)を用いた定義
もう一つの方法は、ジェネリクス(Generics)を使用した Array<T> 形式です。
2次元配列の場合は、Array<Array<number>> のように入れ子にして定義します。
// ジェネリクスを用いた2次元配列の定義
const table: Array<Array<string>> = [
["ID", "Name"],
["1", "Alice"],
["2", "Bob"]
];
この記法は、Reactの useState などで型を指定する際に、他のジェネリクス型と一貫性を持たせたい場合に重宝されます。
ただし、多次元になると記述が冗長になりやすいため、基本的には [][] 形式を使い、特殊な事情がある場合に Array<T> を検討するのが一般的です。
型エイリアスによる可読性の向上
複雑な多次元配列を扱う際、型定義をそのまま変数に記述するとコードの可読性が低下します。
TypeScriptの type キーワードを用いて型エイリアスを作成することで、コードの意味を明確にできます。
// 1行分のデータを表す型
type Row = string[];
// 行の集合であるグリッド(2次元配列)を表す型
type Grid = Row[];
const csvData: Grid = [
["header1", "header2"],
["value1", "value2"]
];
このように定義することで、Grid という型名を見ただけで「これは2次元のデータ構造である」という意図が伝わります。
また、将来的に Row の中身を (string | number)[] に変更したい場合も、1箇所の修正で済むため保守性が向上します。
大規模なアプリケーションでは、このように意味のある名前を型に付与することが推奨されます。
多次元配列の初期化における注意点と解決策
多次元配列を初期化する際、JavaScript/TypeScript特有の挙動によってバグが発生しやすいポイントがあります。
特に、特定のサイズで初期化を行う場合には注意が必要です。
配列の参照渡しによる落とし穴
例えば、3×3の0埋めされた2次元配列を作ろうとして、以下のようなコードを書いてしまうことがあります。
// 誤った初期化の方法
const wrongMatrix: number[][] = new Array(3).fill(new Array(3).fill(0));
// 一つの要素を更新してみる
wrongMatrix[0][0] = 9;
console.log(wrongMatrix);
[
[9, 0, 0],
[9, 0, 0],
[9, 0, 0]
]
実行結果を見ると、最初の要素だけを書き換えたつもりが、すべての行の最初の要素が 9 に変わってしまっています。
これは fill() メソッドが、引数に渡された配列の「参照」をコピーしてすべての要素に割り当てるためです。
すべての行が同じ配列の実体を参照しているため、一行を変更すると全行に影響が及びます。
Array.fromメソッドを活用した安全な初期化
この問題を回避し、各行に独立した配列を割り当てるには Array.from() を使用するのが最適です。
// 正しい初期化の方法
const rows = 3;
const cols = 3;
const correctMatrix: number[][] = Array.from({ length: rows }, () =>
Array.from({ length: cols }, () => 0)
);
correctMatrix[0][0] = 9;
console.log(correctMatrix);
[
[9, 0, 0],
[0, 0, 0],
[0, 0, 0]
]
Array.from の第二引数に渡したコールバック関数は、要素ごとに新しく実行されます。
そのため、各行に new Array() 相当の新しいインスタンスが作成され、参照の共有を防ぐことができます。
多次元配列を動的に生成する場合は、必ずこの方法を検討してください。
タプル型を用いた固定長多次元配列
「必ず2×2の行列である」といった具合に、要素数が固定されている場合は「タプル型」を活用するとさらに安全です。
通常の配列型では push や pop が許可されますが、タプル型を使えばサイズを厳格に制限できます。
// 2x2の固定サイズ行列を定義
type Matrix2x2 = [
[number, number],
[number, number]
];
const position: Matrix2x2 = [
[10, 20],
[30, 40]
];
// 以下はエラーになる
// position.push([50, 60]);
// position[0] = [1, 2, 3];
タプル型を使用することで、意図しないデータの追加や削除をコンパイル時点で防ぐことが可能です。
座標データやRGB値の配列など、構造が厳密に決まっているデータセットに最適です。
多次元配列の便利な操作テクニック
多次元配列を効率的に扱うためには、高階関数を組み合わせて使うのがTypeScriptらしい書き方です。
mapメソッドによる入れ子構造の変換
2次元配列のすべての要素を加工したい場合、map を2重に使用します。
例えば、数値の行列をすべて2倍にする処理は以下のようになります。
const numbers: number[][] = [
[1, 2],
[3, 4]
];
const doubled = numbers.map(row =>
row.map(value => value * 2)
);
console.log(doubled);
[
[2, 4],
[6, 8]
]
この方法は元の配列を破壊せず、新しい配列を生成するため、イミュータブル(不変)なデータ操作が求められるReactなどの環境で非常に有効です。
flatメソッドによる次元の削減
多次元配列を1次元配列に変換したいときには flat() メソッドが非常に便利です。
TypeScriptの型定義も、flat() を使うと自動的に推論されます。
const nestedArray: number[][] = [
[10, 20],
[30, 40],
[50]
];
// 1次元に平坦化
const flatArray = nestedArray.flat();
console.log(flatArray);
[10, 20, 30, 40, 50]
flat() の引数に数値を渡すことで、どの深さまで平坦化するかを指定できます。
デフォルトは 1 ですが、3次元配列を1次元にしたい場合は flat(2) と記述します。
このとき、TypeScriptは平坦化された後の型を正しく number[] と解釈してくれます。
読み取り専用の多次元配列
データの整合性を保つために、一度定義した多次元配列を変更禁止にしたい場合があります。
その場合、readonly 修飾子や ReadonlyArray 型を使用します。
// ネストした部分まで読み取り専用にする
type ImmutableMatrix = ReadonlyArray<ReadonlyArray<number>>;
const constants: ImmutableMatrix = [
[1, 1],
[2, 2]
];
// constants[0][0] = 5; // コンパイルエラー
注意点として、const で宣言しても配列の要素(中身)の書き換えは防げません。
型定義に readonly を付与することで初めて、要素の変更を禁止できます。
設定値や定数として多次元配列を定義する際は、この手法を取り入れるのがベストプラクティスです。
実践的なユースケース:スプレッドシート形式のデータ管理
実務でよくある例として、ユーザー一覧のデータを多次元配列で管理し、それをオブジェクトの配列に変換するケースを考えます。
ここでは、型安全性を保ちながらデータを加工する方法を示します。
// ヘッダー付きの生データ(CSVのような構造)
type RawData = (string | number)[][];
const sheet: RawData = [
["name", "age", "email"],
["Tanaka", 25, "tanaka@example.com"],
["Sato", 30, "sato@example.com"]
];
// データを整形する関数
function parseSheet(data: RawData) {
const [headers, ...rows] = data;
return rows.map(row => {
const entry: Record<string, string | number> = {};
headers.forEach((header, index) => {
entry[header.toString()] = row[index];
});
return entry;
});
}
const users = parseSheet(sheet);
console.log(users);
このコードでは、分割代入を用いてヘッダーとデータ行を分離しています。
多次元配列の各要素にアクセスする際も、型定義があるおかげで補完が効き、実行時の型エラーを未然に防ぐことができます。
まとめ
TypeScriptで多次元配列を扱う際は、単に any[] で済ませるのではなく、構造を正確に型定義することが重要です。
基本的な [][] 記法から、型エイリアスを用いた可読性の向上、そして Array.from による安全な初期化など、本記事で紹介したテクニックを駆使することで、堅牢なコードを記述できます。
最後に、多次元配列を扱う際のポイントを整理します。
- 単純な構造なら
T[][]、意味を持たせるなら型エイリアスを使用する。 - 固定長のデータにはタプル型を活用し、サイズの不一致を防ぐ。
- 初期化時の参照共有を避けるため、
fill()ではなくArray.from()を使う。 - データの不変性を守るために
ReadonlyArrayの導入を検討する。
これらの知識を土台として、TypeScriptによる高度なデータ構造の管理をマスターしていきましょう。
多次元配列の型安全性が確保されれば、大規模なデータの加工や複雑なロジックの実装も、より自信を持って進められるはずです。
