TypeScriptはreadonly な引数に通常の配列も渡せる
React + TypeScriptで開発をしていると、関数の引数やコンポーネントのpropsに配列を渡すことはよくあります。
その際、readonly
を付けることで意図しない変更を防げることは広く知られていますが、実はreadonlyな引数には通常の(readonlyでない)配列も渡せることをご存知でしょうか?
これはTypeScriptの部分型(subtype)関係によるもので、readonly配列型は通常の配列型の「制約を強めた型」として扱われるためです。この性質を理解することで、readonlyを活用しつつ、既存のコードとの互換性を保つことができます。
readonlyとは
TypeScriptにはreadonly
という修飾子があり、オブジェクトや配列の要素を変更不可にすることができます。たとえば、以下のような readonly
配列型を定義できます。
const arr: readonly number[] = [1, 2, 3];
arr.push(4); // エラー
arr[0] = 10; // エラー
このように readonly
を付けると、配列の要素を変更する操作がコンパイル時にエラーとなります。
readonlyな引数に通常の配列を渡すことはできる?
結論から言うと、readonlyな引数には通常の配列を渡すことが可能 です。これはTypeScriptの部分型関係によるもので、readonlyでない配列型(T[]
)はreadonly配列型(readonly T[]
)の部分型として扱われるためです。
具体的なコードを見てみましょう。
const printNumbers = (nums: readonly number[]) => {
console.log(nums);
};
const myNumbers: number[] = [1, 2, 3];
printNumbers(myNumbers); // OK
このコードは問題なくコンパイルされます。myNumbers
はnumber[]
ですが、readonly number[]
への代入が許可されています。
しかし、関数内でnums
を変更しようとするとコンパイルエラーになります。
const printNumbers = (nums: readonly number[]) => {
nums.push(100); // エラー!
};
この性質のおかげで、readonlyを付けることで「この関数は配列を変更しない」という制約を強化しつつ、通常の配列を引数として受け取ることができるため、既存のコードとの互換性が維持できます。
readonlyを使うべき理由
1. 予期しないデータの変更を防ぐ
関数やコンポーネントが受け取った配列を変更してしまうと、呼び出し元の状態が意図せず変わる可能性があります。
const updateArray = (arr: number[]) => {
arr.push(100); // 呼び出し元の配列が変更される
};
const nums = [1, 2, 3];
updateArray(nums);
console.log(nums); // [1, 2, 3, 100]
このような変更を防ぐために readonly
を付けると、配列の変更がコンパイル時にエラーになります。
const updateArray = (arr: readonly number[]) => {
arr.push(100); // エラー
};
2. 意図を明確にする
readonly
を指定することで、「この関数やコンポーネントは配列を変更しない」という意図を明示できます。可読性が向上し、チーム開発においてもコードの意図を理解しやすくなります。
また、配列が変更される可能性を排除できる点でも、コードを読むうえでの負荷が減ります。
3. 互換性を保ちつつ型安全性を向上できる
通常の配列をそのまま渡せるため、例えばReactコンポーネントのpropsの配列をreadonlyに変更しても既存のコードを大きく修正する必要がなく、型安全性を向上させることができます。
readonlyを活用するべきケース
-
関数の引数に配列を渡すとき
const processNumbers = (numbers: readonly number[]) => { console.log(numbers.length); };
-
コンポーネントの props に配列を渡すとき
type Props = { items: readonly string[]; }; const List: React.FC<Props> = ({ items }) => { return ( <ul> {items.map((item) => ( <li key={item}>{item}</li> ))} </ul> ); };
まとめ
readonlyを使うことで、
- 意図しないデータの変更を防げる
- コードの意図を明確にし、可読性を向上させる
- 通常の配列もそのまま渡せるため、互換性を保ちつつ型安全性を向上できる
この「readonlyな引数に通常の配列を渡せる」という性質を知っておくと、TypeScriptをより柔軟に活用できるようになります。
Discussion