はじめに

TypeScriptでアプリケーションを開発する際、複数の値をまとめて扱う「コレクション」は欠かせません。JavaScriptでは配列に自由にどんな値でも入れられますが、TypeScriptでは配列やタプルに型を付けることで、予期しない値の混入やバグを防げます。

本記事では、TypeScriptの配列型の定義方法、読み取り専用配列、タプル型、可変長タプル、そして配列メソッドと型推論の関係について詳しく解説します。

この記事を読み終えると、以下のことができるようになります。

  • TypeScriptの配列型をT[]Array<T>の2つの記法で正しく定義できる
  • 読み取り専用配列(readonly)で意図しない変更を防げる
  • タプル型で固定長・異種要素の配列を型安全に扱える
  • 可変長タプルを使って柔軟な型定義ができる
  • 配列メソッド使用時の型推論を理解し、適切に活用できる

実行環境・前提条件

前提知識

  • JavaScriptの配列操作(push、map、filterなど)の基本
  • TypeScriptの基本的な型注釈の書き方(基本型入門を参照)

動作確認環境

ツール バージョン
Node.js 20.x以上
TypeScript 5.7以上
VS Code 最新版

本記事のサンプルコードは、TypeScript Playgroundで動作確認できます。ローカル環境で実行する場合は、開発環境構築ガイドを参照してください。

期待される結果

本記事のコードを実行すると、以下の動作を確認できます。

  • 型注釈を付けた配列への不正な値の代入がコンパイルエラーになる
  • 読み取り専用配列への変更操作がコンパイルエラーになる
  • タプル型で要素数や型の不一致がコンパイルエラーになる

TypeScriptの配列型

TypeScriptでは、配列に含まれる要素の型を指定することで、型安全な配列操作が可能になります。配列型の定義方法には2つの記法があります。

graph TB
    A[TypeScriptの配列型] --> B["T[] 記法"]
    A --> C["Array&lt;T&gt; 記法"]
    B --> D["例: number[]"]
    B --> E["例: string[]"]
    C --> F["例: Array&lt;number&gt;"]
    C --> G["例: Array&lt;string&gt;"]

配列型の基本 - T[]記法

最も一般的な配列型の定義方法は、要素の型の後ろに[]を付ける記法です。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
// 数値の配列
const numbers: number[] = [1, 2, 3, 4, 5];

// 文字列の配列
const fruits: string[] = ["apple", "banana", "orange"];

// 真偽値の配列
const flags: boolean[] = [true, false, true];

// 配列への要素追加(型が一致すればOK)
numbers.push(6);      // OK
fruits.push("grape"); // OK

// 型が一致しない要素の追加はエラー
numbers.push("seven"); // エラー: 型 'string' を型 'number' に割り当てることはできません
fruits.push(123);      // エラー: 型 'number' を型 'string' に割り当てることはできません

TypeScriptの配列型は、配列内のすべての要素が指定した型であることを保証します。これにより、配列を操作する際に型の不一致によるランタイムエラーを防げます。

配列型の基本 - Array<T>記法

もう一つの記法は、ジェネリック型のArray<T>を使う方法です。Tには要素の型を指定します。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
// 数値の配列(Array<T>記法)
const numbers: Array<number> = [1, 2, 3, 4, 5];

// 文字列の配列(Array<T>記法)
const fruits: Array<string> = ["apple", "banana", "orange"];

// オブジェクトの配列
interface User {
  id: number;
  name: string;
}

const users: Array<User> = [
  { id: 1, name: "Alice" },
  { id: 2, name: "Bob" },
];

T[]とArray<T>の使い分け

2つの記法は機能的に同等ですが、状況に応じて使い分けると可読性が向上します。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
// シンプルな配列にはT[]が読みやすい
const scores: number[] = [85, 90, 78];

// ジェネリクスと組み合わせる場合はArray<T>が読みやすい
function getFirstElement<T>(arr: Array<T>): T | undefined {
  return arr[0];
}

// 複雑な型の配列はArray<T>が見やすいこともある
const handlers: Array<(event: MouseEvent) => void> = [];

// 上記をT[]で書くと括弧が複雑になる
const handlers2: ((event: MouseEvent) => void)[] = [];

一般的には、シンプルな配列にはT[]記法を使い、ジェネリクスと組み合わせる場合や複雑な型の配列にはArray<T>記法を使うことが多いです。プロジェクト内では一貫性を保つことが重要です。

配列の型推論

TypeScriptは配列リテラルから型を推論するため、明示的な型注釈が不要な場合も多くあります。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
// 型推論により number[] と推論される
const numbers = [1, 2, 3, 4, 5];

// 型推論により string[] と推論される
const fruits = ["apple", "banana", "orange"];

// 空配列は any[] と推論されるので注意
const empty = []; // any[]

// 空配列には型注釈を付けることを推奨
const emptyNumbers: number[] = [];

空配列を初期化する場合は、型推論がany[]になってしまうため、明示的に型注釈を付けることを推奨します。

多次元配列

配列の配列(多次元配列)も型安全に扱えます。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
// 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],
  ],
];

// Array<T>記法でも書ける
const matrix2: Array<Array<number>> = [
  [1, 2, 3],
  [4, 5, 6],
];

// 行列の要素にアクセス
const element: number = matrix[0][1]; // 2

ユニオン型の配列

複数の型を含む可能性がある配列は、ユニオン型を使って定義します。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
// 数値または文字列の配列
const mixed: (number | string)[] = [1, "two", 3, "four"];

// nullを含む可能性がある配列
const nullable: (string | null)[] = ["hello", null, "world"];

// 配列の要素を使う際は型ガードが必要
mixed.forEach((item) => {
  if (typeof item === "number") {
    console.log(item.toFixed(2)); // numberとして扱える
  } else {
    console.log(item.toUpperCase()); // stringとして扱える
  }
});

読み取り専用配列(readonly)

配列の内容を変更されたくない場合は、読み取り専用配列を使用します。これにより、意図しない変更を防ぎ、コードの安全性が向上します。

readonly修飾子

readonly修飾子を配列型の前に付けると、読み取り専用配列になります。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
// 読み取り専用配列
const numbers: readonly number[] = [1, 2, 3, 4, 5];

// 読み取りはOK
console.log(numbers[0]); // 1
console.log(numbers.length); // 5

// 変更操作はすべてエラー
numbers.push(6);      // エラー: プロパティ 'push' は型 'readonly number[]' に存在しません
numbers.pop();        // エラー: プロパティ 'pop' は型 'readonly number[]' に存在しません
numbers[0] = 10;      // エラー: 読み取り専用プロパティにインデックスシグネチャで代入することはできません
numbers.splice(0, 1); // エラー: プロパティ 'splice' は型 'readonly number[]' に存在しません

readonly配列では、pushpopspliceshiftunshiftなどの破壊的メソッドが使用できなくなります。

ReadonlyArray<T>型

readonly T[]の代わりに、ReadonlyArray<T>型を使うこともできます。

1
2
3
4
5
6
7
8
9
// ReadonlyArray<T>を使用
const fruits: ReadonlyArray<string> = ["apple", "banana", "orange"];

// 読み取りはOK
const first = fruits[0];
fruits.forEach((fruit) => console.log(fruit));

// 変更操作はエラー
fruits.push("grape"); // エラー

readonly T[]ReadonlyArray<T>は完全に同等です。プロジェクトの規約に合わせて選択してください。

読み取り専用配列と通常配列の互換性

読み取り専用配列と通常配列の間には、特定の互換性ルールがあります。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
const mutableNumbers: number[] = [1, 2, 3];
const readonlyNumbers: readonly number[] = [4, 5, 6];

// 通常配列を読み取り専用配列に代入できる
const readonly1: readonly number[] = mutableNumbers; // OK

// 読み取り専用配列を通常配列に代入できない
const mutable1: number[] = readonlyNumbers; // エラー

// 読み取り専用配列を通常配列に代入するには明示的なコピーが必要
const mutable2: number[] = [...readonlyNumbers]; // OK(スプレッド構文でコピー)

読み取り専用配列を通常配列に代入できないのは、代入後に変更される可能性があるためです。必要な場合はスプレッド構文などで明示的にコピーを作成します。

as constアサーション

as constアサーションを使うと、配列リテラルを読み取り専用のタプル型として扱えます。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
// as constで読み取り専用タプルになる
const colors = ["red", "green", "blue"] as const;
// 型: readonly ["red", "green", "blue"]

// 各要素がリテラル型になる
const firstColor = colors[0]; // 型: "red"(stringではなくリテラル型)

// 変更不可
colors.push("yellow"); // エラー
colors[0] = "pink";    // エラー

as constは、設定値やオプションなど、変更されるべきでない固定値の配列を定義するときに便利です。

TypeScriptのタプル型

タプル型は、固定長で各位置の要素の型が決まっている配列です。通常の配列がすべての要素が同じ型であるのに対し、タプルでは各位置に異なる型を指定できます。

graph LR
    A["通常の配列"] --> B["すべての要素が同じ型"]
    C["タプル型"] --> D["各位置ごとに型を指定"]
    B --> E["number[] = [1, 2, 3]"]
    D --> F["[string, number] = ['age', 25]"]

タプル型の基本

タプル型は、角括弧[]内に各要素の型をカンマ区切りで指定します。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
// [名前, 年齢]のタプル
const person: [string, number] = ["Alice", 30];

// 各要素へのアクセス
const name: string = person[0]; // "Alice"
const age: number = person[1];  // 30

// 型が一致していればOK
const person2: [string, number] = ["Bob", 25]; // OK

// 型の順序が異なるとエラー
const person3: [string, number] = [30, "Alice"]; // エラー

// 要素数が異なるとエラー
const person4: [string, number] = ["Alice"]; // エラー
const person5: [string, number] = ["Alice", 30, true]; // エラー

タプル型は、要素の型だけでなく要素の数も型の一部として扱われます。

タプル型の実践的な使用例

タプル型は、関数から複数の値を返すときや、座標のようなペア値を表現するときに便利です。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// 関数から複数の値を返す
function getNameAndAge(): [string, number] {
  return ["Alice", 30];
}

const [name, age] = getNameAndAge();
console.log(`${name} is ${age} years old`);

// 座標を表すタプル
type Point2D = [number, number];
type Point3D = [number, number, number];

const point2d: Point2D = [10, 20];
const point3d: Point3D = [10, 20, 30];

// RGB色を表すタプル
type RGB = [number, number, number];
const red: RGB = [255, 0, 0];
const green: RGB = [0, 255, 0];

// キーと値のペア
type KeyValue<K, V> = [K, V];
const entry: KeyValue<string, number> = ["score", 100];

ReactのuseStateフックも、タプル型を返す典型的な例です。

1
2
3
// useStateの戻り値はタプル型
// const [state, setState] = useState<number>(0);
// 戻り値の型: [number, (value: number) => void]

ラベル付きタプル

TypeScript 4.0以降では、タプルの各要素にラベルを付けることができます。ラベルを付けることで、タプルの各要素が何を表しているかが明確になります。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
// ラベル付きタプル
type Person = [name: string, age: number, isActive: boolean];

const alice: Person = ["Alice", 30, true];

// 関数の引数でラベル付きタプルを使う
function createPerson(...args: [name: string, age: number]): Person {
  return [...args, true];
}

const bob = createPerson("Bob", 25);

// ラベルはドキュメント目的のみで、アクセス方法は変わらない
const name = alice[0]; // "Alice"
const age = alice[1];  // 30

ラベルはIDEでの補完やホバー時のヒントとして表示されるため、コードの可読性が向上します。

オプショナル要素

タプル型では、末尾の要素をオプショナル(省略可能)にできます。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
// 第3要素がオプショナル
type PersonTuple = [string, number, string?];

const alice: PersonTuple = ["Alice", 30, "Engineer"]; // OK
const bob: PersonTuple = ["Bob", 25];                 // OK(第3要素省略)

// オプショナル要素は末尾にのみ配置可能
type InvalidTuple = [string?, number, string]; // エラー:オプショナル要素の後に必須要素は置けない

// 複数のオプショナル要素
type FlexibleTuple = [string, number?, boolean?];

const tuple1: FlexibleTuple = ["hello"];           // OK
const tuple2: FlexibleTuple = ["hello", 42];       // OK
const tuple3: FlexibleTuple = ["hello", 42, true]; // OK

読み取り専用タプル

タプルもreadonly修飾子で読み取り専用にできます。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
// 読み取り専用タプル
const point: readonly [number, number] = [10, 20];

// 読み取りはOK
console.log(point[0], point[1]);

// 変更はエラー
point[0] = 100; // エラー

// as constでも読み取り専用タプルになる
const config = [3000, "localhost", true] as const;
// 型: readonly [3000, "localhost", true]

可変長タプル(Variadic Tuple Types)

TypeScript 4.0で導入された可変長タプル(Variadic Tuple Types)を使うと、タプルの一部に任意の長さの配列を含めることができます。

レストパラメータを含むタプル

タプル型の中でスプレッド構文(...)を使うと、可変長の要素を表現できます。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
// 先頭が文字列、残りが数値の配列
type StringAndNumbers = [string, ...number[]];

const tuple1: StringAndNumbers = ["sum", 1, 2, 3, 4, 5]; // OK
const tuple2: StringAndNumbers = ["count"];              // OK(数値なし)
const tuple3: StringAndNumbers = ["avg", 10];            // OK

// 先頭と末尾が固定、中間が可変
type Sandwich = [string, ...number[], string];

const sandwich1: Sandwich = ["start", 1, 2, 3, "end"]; // OK
const sandwich2: Sandwich = ["start", "end"];          // OK(中間なし)

関数のレスト引数との組み合わせ

可変長タプルは、関数のレスト引数の型定義で特に有用です。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
// 第1引数が文字列、残りが数値
function logSum(label: string, ...numbers: number[]): void {
  const sum = numbers.reduce((a, b) => a + b, 0);
  console.log(`${label}: ${sum}`);
}

logSum("Total", 1, 2, 3, 4, 5); // "Total: 15"

// タプル型として引数の型を定義
type LogSumArgs = [label: string, ...numbers: number[]];

function logSum2(...args: LogSumArgs): void {
  const [label, ...numbers] = args;
  const sum = numbers.reduce((a, b) => a + b, 0);
  console.log(`${label}: ${sum}`);
}

タプルの結合

スプレッド構文を使って、複数のタプル型を結合できます。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
type First = [string, number];
type Second = [boolean, string];

// 2つのタプルを結合
type Combined = [...First, ...Second];
// 型: [string, number, boolean, string]

const combined: Combined = ["hello", 42, true, "world"];

// ジェネリクスと組み合わせる
type Prepend<T, U extends unknown[]> = [T, ...U];
type PrependedTuple = Prepend<boolean, [string, number]>;
// 型: [boolean, string, number]

配列メソッドと型推論

TypeScriptでは、配列メソッドの戻り値の型が適切に推論されます。これにより、型安全にメソッドチェーンを書けます。

mapメソッドの型推論

mapメソッドは、コールバック関数の戻り値の型に基づいて結果の配列型を推論します。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
const numbers: number[] = [1, 2, 3, 4, 5];

// map: number[] → string[]
const strings = numbers.map((n) => n.toString());
// strings の型: string[]

// map: number[] → { value: number }[]
const objects = numbers.map((n) => ({ value: n }));
// objects の型: { value: number }[]

// 明示的な型パラメータも指定可能
const doubled = numbers.map<number>((n) => n * 2);

filterメソッドと型ガード

filterメソッドは、通常は元の配列と同じ型を返しますが、型ガード関数を使うと型を絞り込めます。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
const mixed: (string | number)[] = [1, "two", 3, "four", 5];

// 通常のfilter: 型は (string | number)[] のまま
const filtered = mixed.filter((item) => typeof item === "number");
// filtered の型: (string | number)[]  ← 絞り込まれない

// 型ガード関数を使用: number[] に絞り込まれる
const numbers = mixed.filter((item): item is number => typeof item === "number");
// numbers の型: number[]

// null/undefinedの除外でよく使うパターン
const nullable: (string | null)[] = ["a", null, "b", null, "c"];

// 型ガードでnullを除外
const nonNull = nullable.filter((item): item is string => item !== null);
// nonNull の型: string[]

型ガード関数(item is numberのような戻り値の型注釈)を使うことで、filterの結果の型を正確に絞り込めます。

reduceメソッドの型注釈

reduceメソッドは、初期値から累積値の型を推論します。初期値を省略する場合や複雑な型の場合は、明示的な型注釈が必要です。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
const numbers: number[] = [1, 2, 3, 4, 5];

// 初期値から型を推論
const sum = numbers.reduce((acc, curr) => acc + curr, 0);
// sum の型: number

// オブジェクトを累積する場合
interface CountMap {
  [key: string]: number;
}

const words = ["apple", "banana", "apple", "cherry", "banana", "apple"];

const wordCount = words.reduce<CountMap>((acc, word) => {
  acc[word] = (acc[word] || 0) + 1;
  return acc;
}, {});
// wordCount の型: CountMap
// 結果: { apple: 3, banana: 2, cherry: 1 }

findメソッドの戻り値

findメソッドは、要素が見つからない可能性があるため、戻り値の型にはundefinedが含まれます。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
interface User {
  id: number;
  name: string;
}

const users: User[] = [
  { id: 1, name: "Alice" },
  { id: 2, name: "Bob" },
];

// find の戻り値は User | undefined
const user = users.find((u) => u.id === 1);
// user の型: User | undefined

// 使用時にはnullチェックが必要
if (user) {
  console.log(user.name); // この時点で user は User 型
}

// オプショナルチェーンも使える
console.log(user?.name);

flatMapメソッド

flatMapmapflatを組み合わせたメソッドで、型推論も適切に行われます。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
const sentences = ["Hello World", "TypeScript is great"];

// flatMap: string[] → string[](単語の配列に展開)
const words = sentences.flatMap((sentence) => sentence.split(" "));
// words の型: string[]
// 結果: ["Hello", "World", "TypeScript", "is", "great"]

// 条件付きで要素を追加または除外
const numbers = [1, 2, 3, 4, 5];
const evenDoubled = numbers.flatMap((n) => (n % 2 === 0 ? [n * 2] : []));
// evenDoubled の型: number[]
// 結果: [4, 8]

配列型の高度なパターン

実務でよく使われる、より高度な配列型のパターンを紹介します。

インデックスアクセス型

配列型から要素の型を取り出すことができます。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
// 配列型から要素の型を取得
type StringArray = string[];
type Element = StringArray[number]; // string

// タプル型から特定位置の型を取得
type Tuple = [string, number, boolean];
type First = Tuple[0];  // string
type Second = Tuple[1]; // number
type Third = Tuple[2];  // boolean

// すべての要素の型をユニオンで取得
type TupleElement = Tuple[number]; // string | number | boolean

配列からユニオン型を生成

as constと組み合わせて、配列リテラルからユニオン型を生成できます。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
// 配列からユニオン型を生成
const statusList = ["pending", "approved", "rejected"] as const;
type Status = (typeof statusList)[number];
// 型: "pending" | "approved" | "rejected"

// 関数の引数として使用
function updateStatus(status: Status): void {
  console.log(`Status updated to: ${status}`);
}

updateStatus("approved"); // OK
updateStatus("invalid");  // エラー: 型 '"invalid"' を型 'Status' に割り当てることはできません

このパターンは、許可された値のリストを一箇所で管理しながら、型としても使えるため非常に便利です。

配列の長さを型として利用

TypeScript 4.1以降では、タプルの長さを型として取得できます。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
// タプルの長さを取得
type Tuple3 = [string, number, boolean];
type Length = Tuple3["length"]; // 3

// 長さを制約として使う
type AtLeast3<T extends unknown[]> = T["length"] extends 0 | 1 | 2 ? never : T;

// 使用例
type Valid = AtLeast3<[1, 2, 3]>;    // [1, 2, 3]
type Invalid = AtLeast3<[1, 2]>;      // never

よくある間違いと対処法

TypeScriptで配列やタプルを扱う際によくある間違いと、その対処法を紹介します。

空配列の型推論

空配列を初期化するとany[]と推論されるため、明示的な型注釈が必要です。

1
2
3
4
5
6
7
8
9
// 悪い例: any[] と推論される
const bad = [];
bad.push(1);
bad.push("string"); // エラーにならない

// 良い例: 明示的に型を指定
const good: number[] = [];
good.push(1);
good.push("string"); // エラー

配列のreadonly忘れ

関数の引数で配列を受け取る際、意図しない変更を防ぐためにreadonlyを付けることを検討してください。

1
2
3
4
5
6
7
8
9
// 悪い例: 引数の配列を変更してしまう可能性がある
function processItems(items: string[]): void {
  items.push("processed"); // 呼び出し元の配列を変更してしまう
}

// 良い例: readonlyで変更を防ぐ
function processItemsSafe(items: readonly string[]): string[] {
  return [...items, "processed"]; // 新しい配列を返す
}

タプルと配列の混同

タプルを返す関数の戻り値の型を配列として定義すると、情報が失われます。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
// 悪い例: 配列型として定義
function getCoordsBad(): number[] {
  return [10, 20];
}
const [x1, y1, z1] = getCoordsBad(); // z1も number と推論される

// 良い例: タプル型として定義
function getCoordsGood(): [number, number] {
  return [10, 20];
}
const [x2, y2, z2] = getCoordsGood(); // z2 は undefined と推論される

まとめ

本記事では、TypeScriptの配列型とタプル型について詳しく解説しました。

配列型の定義方法として、シンプルなT[]記法とジェネリックのArray<T>記法の2つがあり、状況に応じて使い分けることで可読性が向上します。readonly修飾子やReadonlyArray<T>を使うことで、意図しない配列の変更を防げます。

タプル型は、固定長で各位置の型が決まっている配列を表現するのに最適です。ラベル付きタプル、オプショナル要素、可変長タプルを活用することで、柔軟で型安全なデータ構造を定義できます。

配列メソッドと型推論の関係を理解することで、mapfilterreduceなどのメソッドを型安全に使いこなせるようになります。特にfilterで型を絞り込む際の型ガード関数は、実務で頻繁に使用するテクニックです。

次のステップとして、オブジェクト型やインターフェースの定義方法を学ぶことで、より複雑なデータ構造を型安全に扱えるようになります。

参考リンク