TypeScript Readonlyを使ってオブジェクトのプロパティを変化させない(副作用の防止)

関数の引数でオブジェクトを受け取り、そのオブジェクトの特定のプロパティの値を変更してオブジェクトを返す関数を考えてみます。

type T1 = {
    name: string
    age: number
    favor: {
        name: string
    }
}

const myFunc = (obj: T1, age: number): T1 => {
    obj.age = age
    return obj
}

このコードでは引数の obj の age を変更しています。

つまり、関数内で副作用が発生しています。

意図的に変更したい場合は良いのですが、副作用を発生させたくない場合は Readonly を使ってそれを強制することができます。

const myFunc = (obj: Readonly<T1>, age: number): T1 => {
    obj.age = age // Cannot assign to 'age' because it is a read-only property
    return obj
}

Readonly<T1>により、obj.age に対する変更はコンパイルエラーになります。

これを解消するには以下のように新しいオブジェクトを作成し、その age を変更する書き方にする必要があります。

const myFunc = (obj: Readonly<T1>, age: number): T1 => {
    return {
        ...obj,
        age: age,
    }
}

このように Readonly を使うことで、意図しないオブジェクトのプロパティの変更を防ぐことができます。

注意点として、Readonly を使ってもネストされたプロパティは Readonly 属性にならないことです。

この例でいうと、obj.favor.name は変更できてしまいます。

ネストされたプロパティもすべて Readonly にしたい場合は以下のような型定義をすると良いです。

type T1 = {
    name: string
    age: number
    favor: {
        name: string
    }
}

type DeepReadonly<T> = {
  readonly [P in keyof T]: DeepReadonly<T[P]>;
}

const myFunc = (obj: DeepReadonly<T1>): T1 => {
    obj.favor.name = 'tami' // Cannot assign to 'name' because it is a read-only property.
    return obj
}

上記で DeepReadonly を Readonly へ変更すると、コンパイルエラーにならないことが分かると思います。