1. ホーム
  2. TypeScript

【TypeScript】readonly の使い方|プロパティ・配列を読み取り専用にする方法

Share

オブジェクトのプロパティや配列を「あとから書き換えられないようにしたい」ことがあります。TypeScript では readonly を付けるだけで、その値を読み取り専用にできます。うっかり値を上書きしてしまうミスを、コンパイルの段階で見つけられるのが利点です。この記事では、プロパティや配列を読み取り専用にする方法、まとめて適用する Readonly 型、そして「readonly が効かないように見える」ときの注意点まで、初心者向けに解説します。

readonly でできること

readonly は、変数やプロパティを「一度決めたら変更できない」状態にするための修飾子です。値を書き換えようとすると、TypeScript がコンパイルエラーで教えてくれます。設定値やIDのように「途中で変わってほしくない値」を守るのに役立ちます。まずはプロパティに付ける基本形から見ていきましょう。

プロパティを読み取り専用にする

型やインターフェースのプロパティ名の前に readonly を付けると、そのプロパティは初期化後に代入できなくなります。

readonly-prop.ts
interface User {
  readonly id: number; // 読み取り専用
  name: string;        // これは変更できる
}

const user: User = { id: 1, name: "太郎" };

user.name = "次郎"; // OK
user.id = 2;        // エラー:読み取り専用プロパティのため代入できない

id は生成時に 1 を入れられますが、その後 user.id = 2 のように変更しようとするとエラーになります。「作るときには決められるが、あとからは変えられない」という制約を、型として表現できるわけです。クラスでも同じように、フィールドの前に readonly を付けられます。

readonly-class.ts
class Circle {
  // コンストラクタ内でだけ初期化できる
  readonly radius: number;

  constructor(radius: number) {
    this.radius = radius; // OK(初期化)
  }

  scale(n: number) {
    // this.radius = this.radius * n; // エラー:あとからは変更できない
    return new Circle(this.radius * n); // 新しいインスタンスを返す
  }
}

配列・タプルを読み取り専用にする

配列を読み取り専用にするには、readonly T[] または ReadonlyArray<T> と書きます。どちらも意味は同じです。読み取り専用の配列では、pushpop のように中身を変更するメソッドが使えなくなり、要素への代入もできなくなります。

readonly-array.ts
const nums: readonly number[] = [1, 2, 3];

console.log(nums[0]);   // 1(読み取りはできる)
console.log(nums.length); // 3

nums.push(4);  // エラー:push は readonly 配列には存在しない
nums[0] = 10;  // エラー:要素への代入もできない

// map など「新しい配列を返す」メソッドは使える
const doubled = nums.map((n) => n * 2); // [2, 4, 6]

mapfilter のように「元の配列を変えず、新しい配列を返す」メソッドは問題なく使えます。読み取り専用の配列は、関数の引数として「この配列は中で書き換えません」と宣言したいときにも便利です。タプル型でも同様に readonly [number, string] と書けます。

Readonly 型でまとめて適用する

プロパティを1つずつ readonly にする代わりに、標準のユーティリティ型 Readonly<T> を使うと、すべてのプロパティをまとめて読み取り専用にできます。

readonly-utility.ts
interface Config {
  host: string;
  port: number;
}

// すべてのプロパティが readonly になった型
const config: Readonly<Config> = { host: "localhost", port: 8080 };

config.port = 3000; // エラー:読み取り専用プロパティ

readonly が守ってくれない範囲

ネストしたオブジェクトまでは守られない(浅い)

readonlyReadonly<T> が保護するのは、一番外側のプロパティだけです。プロパティの中にあるオブジェクトや配列の中身までは読み取り専用になりません。これを「浅い(shallow)」保護と言います。

shallow.ts
interface State {
  user: { name: string };
}

const state: Readonly<State> = { user: { name: "太郎" } };

state.user = { name: "次郎" };  // エラー:user 自体は読み取り専用
state.user.name = "次郎";       // OK になってしまう(中身までは守られない)

ネストした値まで守りたい場合は、内側の型にも Readonly を適用するなど、階層ごとに指定する必要があります。「readonly を付けたのに中身が書き換えられた」と感じたときは、この「浅い」性質を思い出してください。

実行時には効かない(コンパイル時だけのチェック)

readonly はあくまで TypeScript の型チェックの機能で、コンパイルして JavaScript になった時点で消えます。つまり実行時に値の書き換えを止める効果はありません。JavaScript として本当に変更を禁止したい場合は、Object.freeze を使います。readonly は「開発中のうっかりミスを防ぐ」もの、Object.freeze は「実行時に凍結する」もの、と役割を分けて考えるとよいでしょう。

まとめ

readonly は、プロパティや配列を読み取り専用にして、意図しない書き換えをコンパイル時に防ぐための修飾子です。プロパティには名前の前に付け、配列は readonly T[]、まとめて適用したいときは Readonly<T> を使います。ただし保護されるのは一番外側だけ(浅い)で、実行時には効かない点に注意してください。「変わってほしくない値」に readonly を付けておくだけで、バグを未然に防ぎ、コードの意図も読み手に伝わりやすくなります。

参考ページ