TypeScript | 基礎文法:配列・タプル – タプルとreadonly

TypeScript
スポンサーリンク

タプルとreadonlyの関係をざっくりイメージする

タプルは「要素の数・順番・型が決まっている小さなセット」でした。
ここに readonly を組み合わせると、「形も中身も変えない前提のタプル」を作れます。

つまり、

  • タプル:この位置にこの型、この数だけ
  • readonlyタプル:さらに「あとから書き換え禁止」

という感じで、「動かないデータの塊」を型で表現できるようになります。


readonlyタプルの基本構文

readonly [T1, T2, …] という書き方

タプルに readonly を付けると、「要素の書き換え・追加・削除」ができなくなります。

let user: readonly [string, number] = ["Taro", 20];

const name = user[0]; // 読み取りはOK
const age = user[1];

// user[0] = "Hanako"; // エラー:書き換え禁止
// user.push(30);      // エラー:要素追加も禁止
TypeScript

普通のタプル [string, number] なら user[0] = "Hanako" はOKですが、
readonly [string, number] にすると「読むだけOK・書き込み禁止」になります。

readonlyを付ける意味

ここで大事なのは、「このタプルは“固定された情報”として扱いたい」という意図を型に刻めることです。
「このセットは一度決めたら変えない」という前提があるなら、readonly を付けることで、
将来の自分や他の開発者の“うっかり変更”をコンパイル時に止められます。


as const とタプルとreadonlyの関係

as const は「readonlyタプル+リテラル型」にしてくれる

配列リテラルに as const を付けると、TypeScriptはそれを「readonlyなタプル」として扱います。

const STATUS = ["success", "error"] as const;
// 型: readonly ["success", "error"]

// STATUS[0] = "ok";      // エラー:書き換え禁止
// STATUS.push("pending"); // エラー:追加も禁止
TypeScript

ここで起きていることは、

  • 要素数は2つに固定
  • 1番目は “success”、2番目は “error” というリテラル型
  • readonly なので変更不可

という、かなりガチガチな「固定タプル」化です。
「この並びそのものが仕様だ」というときには、as const はほぼ必須レベルで便利です。

手書きのreadonlyタプルとの違い

手で書くとこうなります。

const STATUS2: readonly ["success", "error"] = ["success", "error"];
TypeScript

as const は、これを自動でやってくれているイメージです。
なので、「リテラルを書いて、その形をそのまま固定したい」ときは、
型注釈よりも as const の方が圧倒的に楽でミスも減ります。


関数の引数・戻り値でのreadonlyタプルの使いどころ

引数にreadonlyタプルを使う(壊さない約束)

function printUser(user: readonly [string, number]) {
  const [name, age] = user;
  console.log(`${name} (${age})`);
  // user[0] = "Hanako"; // エラー:壊せない
}

const u: [string, number] = ["Taro", 20];
printUser(u);
TypeScript

ここで readonly [string, number] にしておくと、
この関数の中から user を書き換えようとした瞬間にエラーになります。

呼び出し側から見ると、「この関数にタプルを渡しても、中身をいじられない」と分かるので、
安心してデータを渡せます。
「引数は読むだけで、絶対に壊さない」という意図を型で表現できるのが、readonlyタプルの強みです。

戻り値にreadonlyタプルを使う(“結果セット”を固定する)

function splitName(fullName: string): readonly [string, string] {
  const [first, last] = fullName.split(" ");
  return [first, last] as const;
}

const result = splitName("Taro Yamada");
// result[0] = "Hanako"; // エラー:戻り値も書き換え禁止
TypeScript

「この関数が返す2つの値は、結果としてのセットであって、
呼び出し側で書き換えるべきものではない」というときに、
readonlyタプルを戻り値に使うと、その意図がはっきり伝わります。


readonlyタプルと普通のタプル・配列の関係

普通のタプル → readonlyタプル に“格上げ”するのは安全

let t: [string, number] = ["Taro", 20];
let r: readonly [string, number] = t; // OK
TypeScript

「変更可能なタプル」を「読み取り専用として扱う」のは安全なので、これは許されます。
「可変なものを、あえて不変として扱う」イメージです。

readonlyタプル → 普通のタプル に“格下げ”するのは危険

const r: readonly [string, number] = ["Taro", 20];
// let t: [string, number] = r; // エラー
TypeScript

「読み取り専用のもの」を「変更可能」として扱うのは危険なので、そのままでは代入できません。
どうしても変更したいなら、コピーして新しいタプルや配列を作るのが安全です。

const t: [string, number] = [...r]; // スプレッドでコピー
TypeScript

「元は守りつつ、必要なところだけコピーして自由にいじる」というスタイルは、
バグを減らすうえでもかなり効きます。


初心者がまず掴んでおきたい「タプル×readonly」の感覚

タプルは「位置と数と型が決まったセット」。
そこに readonly を足すと、「そのセットを壊さない」という約束まで型に含められます。

だからこそ、

  • 仕様として形が決まっている定数(ステータス一覧、ロール一覧など)
  • 関数の引数として「読むだけ」のタプル
  • 関数の戻り値として「結果セット」として扱いたいタプル

こういうところに readonlyタプルを使うと、
「このデータはこういう形で、こういう扱い方しかしない」という設計が、
そのまま型に刻まれていきます。

「このタプル、変えられたら困るよな」と感じたら、
一度 readonly [ ... ]as const を付けてみる。
そこから、「不変なデータ構造を型で守る」という感覚が、じわじわ自分のものになっていきます。

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