- TypeScript
TypeScript readonlyの使い方|const・as const比較
TypeScriptのreadonlyとは、プロパティや配列への書き込みをコンパイル時に止める仕組みです。 変数の再代入を止める const とは守る対象が違い、const は「変数」、readonly は「プロパティ」、as const は「リテラル全体(ネストの奥まで)」を読み取り専用にします。
この記事では、const / readonly修飾子 / Readonly<T> / ReadonlyArray / as const の5つを、「再代入・プロパティ変更・ネスト・配列のpush・型の絞り込み」の5つの軸で早見表にまとめます。各パターンで出るコンパイルエラー文(TS2540、TS2339、TS2588、TS2542、TS4104 など)は、TypeScript 7.0.2 の tsc --noEmit で実際に出したものをそのまま載せました。readonly が1階層目にしか効かない例と、ネストの奥まで読み取り専用にする DeepReadonly のコピペ用型定義も用意しています。
readonly・const・as const の早見表
「止まる」はコンパイルエラーになる、「通る」はエラーにならないという意味です。
| 書き方 | 変数の再代入 | プロパティ変更 | ネストしたプロパティ変更 | 配列のpush | 型の絞り込み(リテラル型) |
|---|---|---|---|---|---|
const x = {...} |
止まる(TS2588) | 通る | 通る | 通る | 文字列・数値を直接入れた変数だけ絞られる |
readonly 修飾子(プロパティ単位) |
対象外 | 付けたプロパティだけ止まる(TS2540) | 通る | 通る | 変わらない |
Readonly<T> |
対象外 | 全プロパティで止まる(TS2540) | 通る | 通る | 変わらない |
ReadonlyArray<T> / readonly T[] |
対象外 | 要素の入れ替えが止まる(TS2542) | 要素オブジェクトの中身は通る | 止まる(TS2339) | 変わらない |
as const |
対象外(const と組み合わせる) | 止まる(TS2540) | 止まる(TS2540) | 止まる(TS2339) | すべてリテラル型になる |
DeepReadonly<T>(自作) |
対象外 | 止まる | 止まる | 止まる | 変わらない |
迷ったときの選び方は次のとおりです。
| やりたいこと | 選ぶもの |
|---|---|
| 変数に別の値を入れ直せないようにしたい | const |
| 型定義の一部のプロパティ(id など)だけ守りたい | readonly 修飾子 |
| 既にある型を、まるごと読み取り専用として受け取りたい | Readonly<T> |
| 引数の配列を関数の中で書き換えないと約束したい | readonly T[] |
| 設定値や定数の一覧を、値ごと固定したい | as const |
| ネストの奥まで読み取り専用にしたい | as const(リテラルの場合)または DeepReadonly<T> |
検証環境とエラーコードの一覧
この記事のエラー文は、次の環境で実際に出力したものです。
npm i typescript # 2026-10-03 時点の latest は 7.0.2
npx tsc -v # Version 7.0.2
npx tsc --noEmit --strict --target ES2023 --pretty false ファイル名.ts
登場するエラーコードを先にまとめます。エラー文で検索してこの記事にたどり着いた方は、ここから該当の章へ進んでください。
| コード | エラー文 | 出る場面 |
|---|---|---|
| TS2588 | Cannot assign to 'user' because it is a constant. | const の変数に再代入した |
| TS2540 | Cannot assign to 'id' because it is a read-only property. | readonly なプロパティに代入した |
| TS2339 | Property 'push' does not exist on type 'readonly number[]'. | 読み取り専用の配列で push や sort を呼んだ |
| TS2542 | Index signature in type 'readonly number[]' only permits reading. | 読み取り専用の配列に添字で代入した |
| TS4104 | The type 'readonly number[]' is 'readonly' and cannot be assigned to the mutable type 'number[]'. | 読み取り専用の配列を、普通の配列型に渡した |
| TS2704 | The operand of a 'delete' operator cannot be a read-only property. | readonly なプロパティを delete した |
| TS1355 | A 'const' assertion can only be applied to references to enum members, or string, number, boolean, array, or object literals. | 変数に対して as const を付けた |
const:変数の再代入を止める
const は JavaScript の機能で、変数に別の値を入れ直すことを止めます。 オブジェクトの中身までは守りません。
// a_const.ts
const user = { name: "Sato", tags: ["a"] };
user = { name: "Kato", tags: [] }; // 再代入
user.name = "Kato"; // プロパティ変更は通る
user.tags.push("b"); // push も通る
実行結果(tsc 7.0.2):
a_const.ts(2,1): error TS2588: Cannot assign to 'user' because it is a constant.
エラーは2行目の再代入だけです。3行目と4行目は通りました。「const で宣言したのにオブジェクトが書き換わった」というときは、この性質が理由です。プロパティを守るには、次の readonly を使います。
readonly修飾子:プロパティ単位で読み取り専用にする
型定義に書く
readonly は TypeScript の機能で、プロパティ名の前に付けると、そのプロパティへの代入がコンパイルエラーになります。
// b_modifier.ts
type User = {
readonly id: number;
name: string;
readonly address: { city: string };
readonly tags: string[];
};
const u: User = { id: 1, name: "Sato", address: { city: "Tokyo" }, tags: ["a"] };
u.id = 2; // readonly プロパティ
u.name = "Kato"; // OK
u.address = { city: "Osaka" }; // readonly プロパティ
u.address.city = "Osaka"; // ネストは通る
u.tags.push("b"); // 配列の中身も通る
u.tags = []; // readonly プロパティ
class Task {
readonly id: number;
constructor(id: number, readonly title: string) { this.id = id; }
rename(t: string) { this.title = t; }
}
実行結果:
b_modifier.ts(8,3): error TS2540: Cannot assign to 'id' because it is a read-only property.
b_modifier.ts(10,3): error TS2540: Cannot assign to 'address' because it is a read-only property.
b_modifier.ts(13,3): error TS2540: Cannot assign to 'tags' because it is a read-only property.
b_modifier.ts(18,28): error TS2540: Cannot assign to 'title' because it is a read-only property.
ここで大事なのは、エラーにならなかった11行目と12行目です。
| 行 | コード | 結果 | 理由 |
|---|---|---|---|
| 8 | u.id = 2 |
止まる | id に readonly が付いている |
| 9 | u.name = "Kato" |
通る | name には付けていない |
| 10 | u.address = {...} |
止まる | address そのものの入れ替えは readonly の対象 |
| 11 | u.address.city = "Osaka" |
通る | city には readonly が付いていない |
| 12 | u.tags.push("b") |
通る | tags の型は普通の string[] のまま |
| 13 | u.tags = [] |
止まる | tags そのものの入れ替えは readonly の対象 |
readonly が守るのは「そのプロパティに何が入っているか」だけで、入っているオブジェクトや配列の中身は別扱いです。 これが「readonly は浅い」と言われる理由です。対処は後半の DeepReadonly の章で扱います。
クラスで使う
クラスでは、readonly なプロパティに代入できるのは宣言時の初期値とコンストラクターの中だけです。上のコードの Task では、コンストラクター内の this.id = id は通り、メソッド rename の中の this.title = t が TS2540 になりました。constructor(readonly title: string) のように引数に付ける書き方は、プロパティの宣言と代入を1行にまとめたものです。
コンパイル後の JavaScript には残らない
readonly は型の情報なので、コンパイルすると消えます。上のファイルを tsc --target ES2022 で出力した結果です。
"use strict";
const u = { id: 1, name: "Sato", address: { city: "Tokyo" }, tags: ["a"] };
u.id = 2; // readonly プロパティ
u.name = "Kato"; // OK
// (中略)
class Task {
title;
id;
constructor(id, title) {
this.title = title;
this.id = id;
}
rename(t) { this.title = t; }
}
readonly は「コンパイル時に書き間違いを見つける仕組み」であり、実行時に書き込みを止める機能ではありません。 実行時にも止めたい場合は Object.freeze を使います(後述)。
ユーティリティ型 Readonly:全プロパティをまとめて読み取り専用にする
Readonly<T> は、型 T のすべてのプロパティに readonly を付けた型を作ります。型定義を書き換えずに、「この場所では読み取り専用として扱う」と指定できます。
// c_utility.ts
type Item = { id: number; name: string; meta: { stock: number }; sizes: string[] };
const item: Readonly<Item> = { id: 1, name: "pen", meta: { stock: 3 }, sizes: ["S"] };
item.id = 2;
item.name = "ink";
item.meta.stock = 0; // ネストは通る
item.sizes.push("M"); // 配列の中身も通る
delete item.name;
実行結果:
c_utility.ts(3,6): error TS2540: Cannot assign to 'id' because it is a read-only property.
c_utility.ts(4,6): error TS2540: Cannot assign to 'name' because it is a read-only property.
c_utility.ts(7,8): error TS2704: The operand of a 'delete' operator cannot be a read-only property.
item.meta.stock = 0 と item.sizes.push("M") は通りました。Readonly<T> も1階層目だけです。中身は次のような定義(マップ型)なので、readonly 修飾子を全プロパティに手で書いたのと同じ結果になります。
type Readonly<T> = { readonly [P in keyof T]: T[P] };
よく使うのは、関数の引数です。
function printItem(item: Readonly<Item>) {
// item.name = "x"; と書くと TS2540(実際に確認済み)
console.log(item.name);
}
呼び出す側は普通の Item をそのまま渡せます。「この関数は受け取ったオブジェクトを書き換えない」という約束を、型で表せます。
ReadonlyArray:配列の push や並べ替えを止める
2つの書き方と、止まる操作
配列を読み取り専用にするには ReadonlyArray<T> または readonly T[] と書きます。2つは同じ型です。
// d_array.ts
const a: ReadonlyArray<number> = [1, 2, 3];
const b: readonly number[] = [1, 2, 3];
a.push(4);
b[0] = 9;
b.sort();
b.length = 0;
const c = [...b].sort((x, y) => y - x); // コピーしてから並べ替え
const d = b.toSorted((x, y) => y - x); // ES2023 の非破壊メソッド
const m: number[] = b; // readonly → 普通の配列には渡せない
function sum(xs: number[]) { return xs.reduce((p, q) => p + q, 0); }
sum(b);
const t: readonly [string, number] = ["x", 1];
t[0] = "y";
const s: ReadonlySet<string> = new Set(["a"]);
s.add("b");
const mp: ReadonlyMap<string, number> = new Map();
mp.set("a", 1);
実行結果:
d_array.ts(3,3): error TS2339: Property 'push' does not exist on type 'readonly number[]'.
d_array.ts(4,1): error TS2542: Index signature in type 'readonly number[]' only permits reading.
d_array.ts(5,3): error TS2339: Property 'sort' does not exist on type 'readonly number[]'.
d_array.ts(6,3): error TS2540: Cannot assign to 'length' because it is a read-only property.
d_array.ts(9,7): error TS4104: The type 'readonly number[]' is 'readonly' and cannot be assigned to the mutable type 'number[]'.
d_array.ts(11,5): error TS4104: The type 'readonly number[]' is 'readonly' and cannot be assigned to the mutable type 'number[]'.
d_array.ts(13,3): error TS2540: Cannot assign to '0' because it is a read-only property.
d_array.ts(15,3): error TS2339: Property 'add' does not exist on type 'ReadonlySet<string>'.
d_array.ts(17,4): error TS2339: Property 'set' does not exist on type 'ReadonlyMap<string, number>'.
エラー文の違いに注目すると、仕組みが見えてきます。
| 操作 | コード | エラー文の意味 |
|---|---|---|
push / sort / add / set |
TS2339 | メソッド自体が型に存在しない。読み取り専用の型には、書き換えるメソッドが定義されていない |
| 添字で代入(配列) | TS2542 | 添字(インデックスシグネチャ)が読み取り専用 |
| 添字で代入(タプル) | TS2540 | タプルの各要素は「0」「1」という名前の readonly プロパティ |
length に代入 |
TS2540 | length が readonly プロパティ |
| 普通の配列型へ渡す | TS4104 | 渡した先で書き換えられるおそれがあるため |
ReadonlyArray<number> と書いても、エラー文では readonly number[] と表示されます。同じ型だからです。なお Readonly<number[]> と書いた場合も、エラー文は readonly number[] でした。
並べ替えたいときの直し方
読み取り専用の配列を並べ替えたいときは、コピーしてから並べ替えるか、元の配列を変えないメソッドを使います。上のコードの7〜8行目はどちらもエラーになっていません。
| 直す前(止まる) | 直した後 | 補足 |
|---|---|---|
b.sort() |
[...b].sort() |
スプレッドでコピーを作る |
b.sort() |
b.toSorted() |
ES2023。--target ES2022 では TS2339 になったため、--target ES2023 以上で使う |
b.push(4) |
[...b, 4] |
新しい配列を作る |
b[0] = 9 |
b.with(0, 9) |
ES2023。[9, 2, 3] が返り、元の配列は変わらない |
b.reverse() |
b.toReversed() |
ES2023。[3, 2, 1] が返る |
TS4104 の直し方:受け取る側を readonly にする
TS4104 は、読み取り専用の配列を number[] を受け取る関数に渡したときに出ます。受け取る側の引数を readonly number[] にするのが素直な直し方です。
// 直す前: function sum(xs: number[]) { ... }
function sum(xs: readonly number[]) { return xs.reduce((p, q) => p + q, 0); }
逆方向は制限がありません。普通の配列は、読み取り専用の引数にそのまま渡せます(下の show(names) はエラーにならず、実行すると a,b が出力されました)。
function show(list: readonly string[]) { return list.join(","); }
const names = ["a", "b"];
console.log(show(names)); // a,b
書き換えない引数は readonly で受けておくと、呼び出す側は普通の配列も読み取り専用の配列も渡せるようになります。 引数を readonly にして損をする場面は少ないので、迷ったら付けておくのがおすすめです。関数の中で prices.push(0) のように書き換えようとすると、TS2339 で気づけます。
配列の要素がオブジェクトのとき
読み取り専用の配列でも、要素のオブジェクトの中身は書き換えられます。
const list: readonly { done: boolean }[] = [{ done: false }];
list[0].done = true; // 通る
list[0] = { done: true }; // エラー
h_more.ts(4,1): error TS2542: Index signature in type 'readonly { done: boolean; }[]' only permits reading.
要素の中身まで守るには readonly Readonly<{ done: boolean }>[] のように両方へ付けるか、DeepReadonly を使います。
as const:ネストの奥まで固定し、型をリテラルに絞る
止まる操作
as const(const アサーション)は、オブジェクトや配列のリテラルに付けると、ネストの奥まですべて readonly にし、さらに値の型をリテラル型に絞ります。
// e_asconst.ts
const conf = { env: "dev", port: 8080, db: { host: "localhost" }, roles: ["admin", "user"] } as const;
conf = {} as any;
conf.port = 9090;
conf.db.host = "example";
conf.roles.push("guest");
conf.roles[0] = "owner";
type Role = (typeof conf.roles)[number];
const r: Role = "guest";
const plain = { env: "dev" };
const fixed = { env: "dev" } as const;
function start(env: "dev" | "prod") {}
start(plain.env);
start(fixed.env);
実行結果:
e_asconst.ts(2,1): error TS2588: Cannot assign to 'conf' because it is a constant.
e_asconst.ts(3,6): error TS2540: Cannot assign to 'port' because it is a read-only property.
e_asconst.ts(4,9): error TS2540: Cannot assign to 'host' because it is a read-only property.
e_asconst.ts(5,12): error TS2339: Property 'push' does not exist on type 'readonly ["admin", "user"]'.
e_asconst.ts(6,12): error TS2540: Cannot assign to '0' because it is a read-only property.
e_asconst.ts(8,7): error TS2322: Type '"guest"' is not assignable to type '"admin" | "user"'.
e_asconst.ts(12,7): error TS2345: Argument of type 'string' is not assignable to parameter of type '"dev" | "prod"'.
conf.db.host のようなネストしたプロパティも、配列の push も止まりました。2行目の TS2588 は const によるもので、as const 自体は変数の再代入を止めません。const と as const を組み合わせて、変数と中身の両方を固定するのが定番の形です。
型がどう変わるか
as const のもう1つの効果が、型の絞り込みです。同じ値でも、推論される型が変わります。コンパイラーのエラー文から読み取った実際の型は次のとおりです。
| 宣言 | 推論された型 |
|---|---|
const a = { env: "dev", port: 8080, roles: ["admin", "user"] } |
{ env: string; port: number; roles: string[]; } |
同じ値に as const |
{ readonly env: "dev"; readonly port: 8080; readonly roles: readonly ["admin", "user"]; } |
const c: Readonly<typeof a> = a |
Readonly<{ env: string; port: number; roles: string[]; }> |
as const を付けると、env は string ではなく "dev" というリテラル型になり、roles は長さと順序の決まった読み取り専用のタプルになります。Readonly<T> は readonly を付けるだけで、型は string や string[] のままです。
上のコードの12行目(TS2345)はこの違いによるものです。plain.env は string 型なので "dev" | "prod" を受け取る関数に渡せず、fixed.env は "dev" 型なので渡せます。
値の一覧から型を作る
as const でリテラル型になると、値の一覧から型を取り出せます。 値と型を二重に書かずに済みます。
// 配列からユニオン型を作る
const roles = ["admin", "user"] as const;
type Role = (typeof roles)[number]; // "admin" | "user"
// オブジェクトを enum の代わりに使う
const Status = { Draft: "draft", Published: "published" } as const;
type Status = (typeof Status)[keyof typeof Status]; // "draft" | "published"
const st: Status = "archived";
h_more.ts(25,7): error TS2322: Type '"archived"' is not assignable to type 'Status'.
as const satisfies:形の確認と値の固定を両立する
as const だけでは、「このオブジェクトが決まった形に沿っているか」は確認されません。satisfies を組み合わせると、形の確認をしつつ、リテラル型も保てます。
type Route = { path: string; auth: boolean };
const routes = {
home: { path: "/", auth: false },
admin: { path: "/admin", auth: true },
} as const satisfies Record<string, Route>;
type RouteName = keyof typeof routes; // "home" | "admin"
const adminPath: "/admin" = routes.admin.path; // リテラル型のまま(エラーにならない)
routes.home.auth = true;
h_more.ts(20,13): error TS2540: Cannot assign to 'auth' because it is a read-only property.
routes.admin.path が "/admin" 型として扱われ、代入だけが止まりました。
as const を付けられない場所
as const はリテラルに直接付けるものです。既にある変数には付けられません。
const src = ["a"];
const cc = src as const;
let mode = "dark" as const;
mode = "light";
h_more.ts(42,12): error TS1355: A 'const' assertion can only be applied to references to enum members, or string, number, boolean, array, or object literals.
h_more.ts(45,1): error TS2322: Type '"light"' is not assignable to type '"dark"'.
let に as const を付けた場合、再代入そのものは let なので禁止されませんが、型が "dark" に絞られるため、別の文字列を入れると TS2322 になります。
関数の引数で同じ絞り込みをしたいときは、TypeScript 5.0 で加わった const 型パラメーターが使えます。呼び出す側が as const を書かなくても、リテラル型として受け取れます。
function pick<const T extends readonly string[]>(xs: T): T[number] { return xs[0]; }
const picked = pick(["a", "b"]); // 型は "a" | "b"
const bad: "c" = picked;
h_more.ts(30,7): error TS2322: Type '"a" | "b"' is not assignable to type '"c"'.
Type '"a"' is not assignable to type '"c"'.
readonly が浅くしか効かない例と DeepReadonly
浅い readonly のまとめ
ここまでの結果を、ネストの観点で整理します。
| 書き方 | 1階層目 | 2階層目(a.b.c = 1) |
配列プロパティの push |
|---|---|---|---|
readonly 修飾子 |
止まる | 通る | 通る |
Readonly<T> |
止まる | 通る | 通る |
readonly T[] |
止まる | 要素の中身は通る | 止まる |
as const |
止まる | 止まる | 止まる |
DeepReadonly<T> |
止まる | 止まる | 止まる |
as const は奥まで効きますが、リテラルにしか付けられません。API の応答や引数など、型として受け取るデータを奥まで読み取り専用にしたいときは DeepReadonly を自分で定義します。
コピペ用の DeepReadonly 型定義
// f_deep.ts
type DeepReadonly<T> =
T extends (...args: any[]) => any ? T :
T extends ReadonlyMap<infer K, infer V> ? ReadonlyMap<DeepReadonly<K>, DeepReadonly<V>> :
T extends ReadonlySet<infer U> ? ReadonlySet<DeepReadonly<U>> :
T extends object ? { readonly [K in keyof T]: DeepReadonly<T[K]> } :
T;
上から順に、次の判定をしています。
| 順番 | 条件 | 変換 |
|---|---|---|
| 1 | 関数 | そのまま(メソッドを呼べるように残す) |
| 2 | Map | ReadonlyMap にして、キーと値も再帰的に変換 |
| 3 | Set | ReadonlySet にして、要素も再帰的に変換 |
| 4 | それ以外のオブジェクト(配列・タプルを含む) | 全プロパティに readonly を付けて、値も再帰的に変換 |
| 5 | 文字列・数値などのプリミティブ | そのまま |
4番目のマップ型は、配列やタプルに適用すると readonly T[] や読み取り専用のタプルに変換されます。次のコードで確かめます。
type Order = {
id: number;
customer: { name: string; address: { city: string } };
lines: { sku: string; qty: number }[];
tags: Set<string>;
pair: [string, number];
note?: string;
total(): number;
};
declare const order: DeepReadonly<Order>;
order.id = 2;
order.customer.address.city = "Osaka";
order.lines.push({ sku: "A", qty: 1 });
order.lines[0].qty = 5;
order.tags.add("x");
order.pair[0] = "z";
const n: number = order.total(); // メソッドは呼べる
const first: string = order.pair[0]; // タプルの型も保たれる
実行結果:
f_deep.ts(18,7): error TS2540: Cannot assign to 'id' because it is a read-only property.
f_deep.ts(19,24): error TS2540: Cannot assign to 'city' because it is a read-only property.
f_deep.ts(20,13): error TS2339: Property 'push' does not exist on type 'readonly { readonly sku: string; readonly qty: number; }[]'.
f_deep.ts(21,16): error TS2540: Cannot assign to 'qty' because it is a read-only property.
f_deep.ts(22,12): error TS2339: Property 'add' does not exist on type 'ReadonlySet<string>'.
f_deep.ts(23,12): error TS2540: Cannot assign to '0' because it is a read-only property.
3階層目の city、配列の push、配列要素の qty、Set の add、タプルの要素まで、すべて止まりました。20行目のエラー文から、lines の型が readonly { readonly sku: string; readonly qty: number; }[] に変換されていることも読み取れます。メソッド呼び出しとタプルの読み取り(最後の2行)はエラーになっていません。
なお、Date のようにメソッドで自分自身を書き換えるクラスは、この定義では setDate などのメソッドが残ります(関数はそのまま残すため)。DeepReadonly<{ at: Date }> で試すと、ev.at = new Date() は TS2540 で止まり、ev.at.setDate(1) は通りました。日付は文字列やタイムスタンプで持つ、という設計と組み合わせると扱いやすくなります。
知っておきたい3つの性質
readonly の型は、普通の型の変数に代入できてしまう
オブジェクトの readonly は、代入の互換性チェックでは考慮されません。
// g_hole.mts 実行: node g_hole.mts(Node.js 22.22.2 は型を取り除いて実行できる)
type Point = { x: number };
const p: Readonly<Point> = { x: 1 };
const q: Point = p; // エラーにならない
q.x = 99;
console.log(p.x);
実行結果:
99
tsc --noEmit --strict でもこのコードはエラーにならず、p.x は 99 に変わりました。readonly は「この変数を通しては書き換えない」という約束で、別の変数を通した書き換えまでは止めません。 配列の場合は TS4104 で止まるので、オブジェクトと配列で挙動が違う点を覚えておくと安心です。
readonly を外す Mutable 型
マップ型で -readonly と書くと、readonly を外せます。テスト用のデータを組み立てるときなどに使えます。
type Mutable<T> = { -readonly [K in keyof T]: T[K] };
const w: Mutable<Readonly<Point>> = { x: 1 };
w.x = 2; // エラーにならない
実行時にも止めたいときは Object.freeze
readonly と as const はコンパイル後に消えます。実行時にも書き込みを止めたい場合は Object.freeze を使います。
const frozen = Object.freeze({ a: 1, inner: { b: 2 } });
frozen.inner.b = 3;
try { (frozen as any).a = 2; } catch (e) { console.log((e as Error).message); }
console.log(frozen);
const ro = { a: 1 } as const;
(ro as any).a = 2;
console.log(ro);
実行結果(ESモジュールとして実行):
Cannot assign to read only property 'a' of object '#<Object>'
{ a: 1, inner: { b: 3 } }
{ a: 2 }
| 項目 | readonly / as const | Object.freeze |
|---|---|---|
| 効くタイミング | コンパイル時 | 実行時 |
型を迂回した書き込み(as any など) |
通る({ a: 2 } に変わった) |
TypeError になる(厳格モード) |
| ネスト | as const は奥まで、readonly は1階層 | 1階層だけ(inner.b は 3 に変わった) |
| 型への影響 | あり | 戻り値の型が Readonly<T> になる |
Object.freeze も浅い点は同じです。厳格モードでない通常のスクリプトでは、エラーにならず代入が無視されます。
場面別の使い分け
| 場面 | 書き方の例 | ねらい |
|---|---|---|
| ID や作成日時など、作った後に変えない項目 | readonly id: number |
その項目だけ守る |
| 関数の引数(オブジェクト) | (item: Readonly<Item>) |
関数の中で書き換えないと示す |
| 関数の引数(配列) | (xs: readonly number[]) |
push や sort を止め、渡せる配列の幅も広がる |
| 設定値・定数の一覧 | { ... } as const |
値ごと固定し、型も取り出す |
| 選択肢の一覧 | ["a", "b"] as const と (typeof xs)[number] |
値と型を1か所で管理する |
| 形の確認もしたい定数 | as const satisfies 型 |
形の確認とリテラル型の両立 |
| 状態管理や API の応答データ | DeepReadonly<T> |
ネストの奥まで書き換えを止める |
| 外部に公開する共有オブジェクト | Object.freeze も併用 |
実行時にも止める |
よくある質問
Q. const と readonly はどちらを使えばよいですか?
A. 守る対象が違うので、両方使います。変数には const、プロパティには readonly です。const はオブジェクトの中身を守らないため、中身を守りたいときは readonly や as const を足します。
Q. ReadonlyArray<T> と readonly T[] に違いはありますか?
A. 同じ型です。今回の検証でも、どちらで宣言してもエラー文は readonly number[] と表示されました。短く書ける readonly T[] がよく使われます。
Q. as const と Readonly<T> の違いは何ですか?
A. Readonly<T> は1階層目に readonly を付けるだけで、型は string や number のままです。as const はネストの奥まで readonly にし、さらに値を "dev" や 8080 のようなリテラル型に絞ります。as const はリテラルにだけ付けられ、Readonly<T> は型に対して使う、という使いどころの違いもあります。
Q. readonly を付けると実行速度に影響しますか? A. コンパイル後の JavaScript には残らないため、実行時の動作は変わりません。上で示したとおり、出力された JavaScript には readonly の記述がありませんでした。
まとめ
- const は変数の再代入を止める(TS2588)。オブジェクトの中身は守りません。
- readonly 修飾子と
Readonly<T>はプロパティへの代入を止める(TS2540)。効くのは1階層目だけです。 readonly T[]は push や sort を止める(TS2339)。添字の代入は TS2542、普通の配列型へ渡すと TS4104 です。- as const はネストの奥まで readonly にし、型をリテラルに絞る。値の一覧から型を作るときにも使えます。
- 奥まで守りたい型には DeepReadonly。この記事の定義は、配列・タプル・Map・Set・メソッドに対応しています。
- readonly はコンパイル時の仕組み。実行時にも止めたいときは
Object.freezeを併用します。 - 書き換えない引数は readonly で受けると、呼び出す側の自由度が上がり、意図も伝わります。
株式会社RJC ― SI事業・SES事業・AI駆動開発。RJCは一緒に成長を楽しめる会社です。
WE ARE HIRING
RJCで一緒に開発しながら、
成長を楽しみませんか?
RJCは、Web・モバイル・AIを活用した開発プロジェクトで、テックリードやPM・PMOも活躍するシステム開発会社です。会社を知る、待遇を確かめる、話を聞いてみる。気になるところ見てみてください!
- 127日年間休日
- 12時間平均残業時間
- 毎日ガチャ遊びココロも大切にする福利厚生。アマギフなどの賞品ラインナップ!
ほかにも、チケットレストラン、書籍読み放題、2年ごとの慰労報奨(休暇 or 金一封)、11期連続の黒字決算。