タプルと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"];
TypeScriptas 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 を付けてみる。
そこから、「不変なデータ構造を型で守る」という感覚が、じわじわ自分のものになっていきます。
