TL;DR

  • 関数が引数のオブジェクトを変更しないなら、その型を Readonly で包む。関数の契約が明確になり、不用意な変更を防げる。
  • ただし readonly の効果は浅く、プロパティにしか効かない。メソッドによる変更は防げない。

内容

readonly で変更を防ぐ

JavaScript のプリミティブはイミュータブルだが、配列やオブジェクトはミュータブルである。TypeScript では、プロパティに readonly 修飾子を付けると、そのプロパティの変更を型チェックで防げる。Readonly ユーティリティ型を使うと、すべてのプロパティに readonly を付けられる。

そのため、関数がオブジェクトを引数として受け取り、それを変更しないなら、その型を Readonly で包むとよい。関数の契約が明確になり、実装での不用意な変更を防げる。具体的には、引数が関数本体で変更されないことを TypeScript がチェックする。呼び出し元には関数が引数を変更しないことが伝わり、呼び出し元は readonly や Readonly のオブジェクトを関数に渡せるようになる。

readonly は伝染する

ある関数の引数に readonly を付けると、その関数が呼び出す別の関数の引数も readonly にする必要が出てくる。これによって安全性はさらに上がる。しかし、ライブラリの関数のように型宣言を変えられない場合は、型アサーションを使うか、型宣言にパッチを当てる(項目71)必要がある。

注意点 1: 効果が浅い

readonly や Readonly<T> の効果は「浅い」。ネストしたオブジェクトのプロパティは変更できてしまう。

interface Outer {
  inner: { x: number }
}
 
const obj: Readonly<Outer> = { inner: { x: 0 } }
obj.inner = { x: 1 } // 型エラー
obj.inner.x = 1 // これは通る

効果を「深く」するには自分で型を定義する必要があるが、正しく実装するのは難しい。ts-essentials の DeepReadonly のようなライブラリを使うとよい。

import { DeepReadonly } from "ts-essentials"
type DeepReadonlyOuter = DeepReadonly<Outer>

注意点 2: プロパティにしか効かない

readonly はプロパティにしか影響せず、メソッドには影響しない。そのため、オブジェクトを変更するメソッドを呼べば変更できてしまう。

const date: Readonly<Date> = new Date()
date.setFullYear(2038)

一般に、あるクラスのミュータブル版とイミュータブル版は、別々のインタフェースとして定義する必要がある。たとえば lib.es5.d.ts には、Array<T> と ReadonlyArray<T> が別々に定義されている。この 2 つはよく使われるので、T[] と readonly T[] という専用の構文がある。

T[] は readonly T[] の変数に代入できるが、逆はできない(T[] は readonly T[] のサブタイプ)。もし逆ができたら、readonly T[] を変更できてしまう。

キーワード