
TypeScriptのReadonlyとは
TypeScriptのReadonlyは、オブジェクトのプロパティを読み取り専用(変更不可)にするためのユーティリティ型です。
TypeScript
type User = {
name: string
age: number
}
const user: Readonly<User> = {
name: 'Sato',
age: 30
}
user.age = 31 // ❌ エラー
実態としては以下のような型で変更されているのと同じなので、nameを読み取り専用にして、ageを変更可能にしたい場合はreadonlyでできます。
TypeScript
type ReadonlyUser = {
readonly name: string
readonly age: number
}
Readonlyは浅い(shallow)読み取り専用
type ReadonlyUserを見ての通り、Readonlyは浅い(shallow)読み取り専用なので、以下のような形の場合はエラーにはなりません。
TypeScript
type User = {
name: string
age: number,
hobby: string[]
}
const user: Readonly<User> = {
name: 'Taro',
age: 30,
hobby: ['soccer', 'reading']
}
user.hobby.push('cooking')
console.log(user)
TypeScript
type User2 = {
name: string
age: number,
hobby: {
type1: string
type2: string
}
}
const user2: Readonly<User2> = {
name: 'Taro',
age: 30,
hobby: {
type1: 'soccer',
type2: 'reading',
}
}
user2.hobby.type2 = 'cooking'
DeepReadonlyの型を作成する
オブジェクトのネストが浅くない場合は、DeepReadonlyの型を作成して使用すれば前述のような読み取り専用なのに変更してもエラーにならない問題を解決できます。
方法としては、以下のコードでtype DeepReadonlyを定義し、DeepReadonly<User>のように使用するだけです。
TypeScript
type DeepReadonly<T> = {
readonly [P in keyof T]: T[P] extends (...args: unknown[]) => unknown
? T[P]
: T[P] extends object
? DeepReadonly<T[P]>
: T[P]
}
type User = {
name: string
age: number,
hobby: string[]
}
type User2 = {
name: string
age: number,
hobby: {
type1: string
type2: string
}
}
const user: DeepReadonly<User> = {
name: 'Taro',
age: 30,
hobby: ['soccer', 'reading']
}
const user2: DeepReadonly<User2> = {
name: 'Taro',
age: 30,
hobby: {
type1: 'soccer',
type2: 'reading',
}
}
// ❌️ エラーが表示される。
user.hobby.push('cooking')
console.log(user)
// ❌️ エラーが表示される。
user2.hobby.type2 = 'cooking'
console.log(user2)
↑このコードをDeepReadonlyからReadonlyに変えると、エラーが表示されません。
TypeScriptでReadonlyをオブジェクトに付けて、読み取り専用になっていると勘違いされているケースは結構多いです。
最初は浅い(shallow)読み取り専用であっても、あとから拡張されて深い構造になると、Readonlyでは対応できなくなる場合があります。
そのため、最初からDeepReadonlyを使用することをオススメします。