公開 API では T[]
を避け, ReadonlyArray
を使いましょう. 内部実装でのみArray
を使うのはOK
以下のコードには受け取った配列をソートして表示する sortLog 関数と, 0番目の要素を 999 にした配列を表示する setLog 関数を定義して使用しています
const sortLog = (array: Arraynumber>): void => {
console.log(array.sort((a, b) => a - b));
};
const setLog = (array: Arraynumber>): void => {
array[0] = 999;
console.log(array);
};
const array = [2, 1, 3];
sortLog(array);
setLog(array);
console.log(array);
Array.prototype.sort
や array[index] = value
は破壊的変更をする仕様なため, 意図せず呼び出し元の配列を変更してしまいます. これを防ぐために ReadonlyArray
を使います
このように Array.prototype.sort
や array[index] = value
をしようとすると型エラーになり間違いに気づくことができます
破壊的変更をしない代わりのメソッドを使うと以下のようになります
const sortLog = (array: ReadonlyArraynumber>): void => {
console.log(array.toSorted((a, b) => a - b));
};
const setLog = (array: ReadonlyArraynumber>): void => {
console.log(array.with(0, 999));
};
const array: ReadonlyArraynumber> = [2, 1, 3];
sortLog(array);
setLog(array);
console.log(array);
React のコンポーネントの Props など破壊的変更をするを意図していないのなら必ず ReadonlyArray
で指定することを強くおすすめします. そうすることによってArray
を「破壊的変更を意図している」という意味で使うことができます
- Deno KV の Deno.KvKey
- GraphQL サーバーの実装にほぼ使うであろう, 取得をまとめてするためのライブラリ dataloader の DataLoader 定義に使う keys
- サーバーレスデータベースのxata liteのTypeScript SDK のIDの配列から一度に複数取得する
read
メソッド https://github.com/xataio/client-ts/issues/1286 (私が提案しました)
などで使われています
また, ReadonlyArray
の方が必要なメソッドが少ないため, Array
をそのまま受け取ることもできます
const sortLog = (array: ReadonlyArraynumber>): void => {
console.log(array.toSorted((a, b) => a - b));
};
const array: Arraynumber> = [2, 1, 3];
sortLog(array);
console.log(array);
ほとんどの場合 ReadonlyArray
を使えばOKです
Array の方が分かりやすい例
直列でHTTP APIを呼ぶ例です
const apiGenerator = async function* (): AsyncGeneratorstring, void, unknown> {
for (const v of Array.from({ length: 3 }, (_, i) => i)) {
const url = new URL("https://postman-echo.com/get");
url.searchParams.set("v", `${v}`);
yield (await (await fetch(url)).json()).args.v;
}
};
const callApis = async (): PromiseReadonlyArraystring>> =>
await Array.fromAsync(apiGenerator());
console.log(await callApis());
Async Generator を作るのが面倒なのも分かるので, Array
の変数の範囲が関数内に収まるのなら このようにArray
を使っても良いと思います
const callApis = async (): PromiseReadonlyArraystring>> => {
const result: Arraystring> = [];
for (const v of Array.from({ length: 3 }, (_, i) => i)) {
const url = new URL("https://postman-echo.com/get");
url.searchParams.set("v", `${v}`);
result.push((await (await fetch(url)).json()).args.v);
}
return result;
};
console.log(await callApis());
ReadonlyArray
- T[] という配列のための専用構文ではなく, 他のジェネリックを使った指定と揃えることができる
- 入れ子になっていても分かりやすい
type OuterA = readonly number[][]; type OuterB = readonly (number[])[]; type OuterC = ReadonlyArrayArraynumber>>; type InnerA = (readonly number[])[]; type InnerB = ArrayReadonlyArraynumber>>;
OuterA, OuterB, OuterC は外側の配列が読み取り専用で, 内側は変更可能
InnerA, InnerB は外側の配列が変更可能で, 内側は読み取り専用 - VSCodeでCtrl+クリックなどでできる「定義への移動」でメソッドの一覧を見ることができる
要素数が事前に決まっている場合は readonly の記法しか使えないため 仕方ないですが readonly の記法で書きましょう
type Position = readonly [number, number, number];
const a: Position = [1, 2, 3];
ReadonlyArray
の他にも TypeScript の標準ライブラリには ReadonlySet
, ReadonlyMap
があります. 積極的に使いましょう
const readonlySet: ReadonlySetstring> = new Set(["C", "B"]);
console.log(readonlySet.isSubsetOf(new Set(["A", "B", "C"])));
const newSet: ReadonlySetstring> = new Set([...readonlySet, "D"]);
console.log(newSet);
console.log(readonlySet);
const readonlyMap: ReadonlyMapnumber, string> = new Map([
[1, "A"],
[2, "B"],
[3, "C"],
]);
console.log(readonlyMap.get(2));
const newMap: ReadonlyMapnumber, string> = new Map([...readonlyMap, [
2,
"BB",
]]);
console.log(newMap);
console.log(readonlyMap);
ReadonlyArray
に比べて非破壊的に操作するメソッドがないため. 色々操作するときはスコープを関数中に収めて Set
Map
を使いましょう
- Array に対応する ReadonlyArray
- Set に対応する ReadonlySet
- Map に対応する ReadonlyMap
があるなら
- Uint8Array に対応する ReadonlyUint8Array
- URL に対応する ReadonlyURL
などがあっても良いと思われますが, この Issue に書かれているように TypeScript の標準ライブラリには含めない方針のようです
代わりに 私が JSRパッケージを作ったので良かったら使ってくださいね
他のReadonlyの型は適宜追加しようと思います. Issue, Pull Request 歓迎です
オブジェクトのプロパティに対しても readonly
を指定することができます
type Account = {
readonly id: string;
readonly name: string;
};
const account: Account = {
id: crypto.randomUUID(),
name: "A",
};
account.name = "B";
できるだけ readonly
を指定する方が良いと思います. タイプ数を少しでも減らしたい人は Readonly
を使うこともできますが,
- React Props で使ったとき, StoryBook で型が解釈できないため Controlsの表示が一部なくなる
- 再帰的には適用されない
ことに注意が必要です
私の個人のプロジェクトでは読み取り専用としか使わない場合は, 全部 readonly をつけています. 私は少しでも型安全性が上がるならタイプ数が増えても良いと考えていますが, タイプ数や余計な修飾子を付けたくない人もいるようです
Object.freeze
Object.freeze を使って型チェック時だけでなく, 実行時にも読み取り不可にすることもできます
type Account = {
readonly id: string;
readonly name: string;
};
const account: Account = Object.freeze({
id: crypto.randomUUID(),
name: "A",
});
account.name = "B";
有名な読み取り専用のプロパティは window.undefined
ですね
ただ あまり使われないため V8 などの JavaScript 実行エンジンの最適化が発揮されず遅くなることがあります. readonly を使った型チェック時だけでも充分バグを見つけられるため Object.freeze
を使うことは少ないでしょう
JavaScript Records & Tuples Proposal (撤回)
「JavaScript Records & Tuples Proposal」という提案がありましたが 今年 2025年に撤回されました
デフォルトで読み取り専用になるステキな提案でしたが, 新たに構文とプリミティブ型を追加するのはとても大変なので撤回されたのは仕方ないと思います
proposal Composites (Stage 1)
Object.freeze
のようにオブジェクトをつくってから読み取り専用にするアプローチ. 例で挙げられているようにSet
, Map
のキーでの活用が進みそう. それ以外のオブジェクトでは, 実行エンジンの最適化とTypeScriptが対応すれば使われるようになると思います
デフォルトが変更可能になってしまったTypeScriptで, 「Readonlyを使おう」という啓蒙活動をするよりも, デフォルトで読み取り専用になっているRustなどの言語の啓蒙活動のほうが, 各個人が覚えることが少なく 間違うことも減り良い気もする
似たような主張の記事
諦めて readonly を使わない主張の記事
Views: 0