Immutable

イミュータブル

Immutable を分かりやすく

Immutable(イミュータブル)とは、文字通り変異(mutation)しないことで、オブジェクトが一度生成された後、その状態が変更できない特性を指します。この概念はプログラムの予測性を高め、副作用を減らすのに役立ちます。JavaScript では、一般的にオブジェクトや配列はミュータブル(状態変更可能)ですが、イミュータブルなパターンを適用することで、より安全で予測可能なコードを書くことが可能となります。

JavaScript では、文字列や数字は、イミュータブル(変更不可)、配列とオブジェクトはデフォルトでミュータブル(変更可能)になっています。

最近の JavaScript では、ミュータブルが悪とされているので、ミュータブルを避けて、イミュータブルなプログラミングをすることが推奨されています。なので、配列やオブジェクトのコピーや結合には、破壊的メソッドを避けて、スプレッド構文が使われることが多いんですね。

そういったこともあって、近年のプログラミングでは、イミュータビリティが重視される傾向にあります。その理由としては、イミュータビリティがコードの予測性を高め、副作用の発生を防ぐためです。その結果、ミュータブルなデータ構造の操作には破壊的なメソッドを避け、代わりにスプレッド構文などの非破壊的な操作を用いることが推奨されています。

例えば、配列の結合を行う際には以下のようにします。

const array1 = [1, 2, 3];
const array2 = [4, 5, 6];

// 破壊的な方法(ミュータブル)
// array1.push(...array2);

// 非破壊的な方法(イミュータブル)
const array3 = [...array1, ...array2];

このように、スプレッド構文を使用すれば、元の配列を変更せずに新たな配列を作成することができます。このようなイミュータブルな手法は、コードの安全性と予測性を高めます。

Immutable を使うメリット

イミュータブルな設計には以下のようなメリットがあります。

予測可能性: イミュータブルなオブジェクトは一度作成されるとその状態が変更されないため、プログラムの挙動を予測しやすくなります。

副作用の排除: データが変更されると、それに依存する他の部分で予期せぬエラーが発生する可能性があります。しかし、イミュータブルなデータは変更が不可能なため、このような副作用を防ぐことができます。

デバッグの容易性: データの変更が追跡しやすくなり、バグの発見と修正が容易になります。

Immutable を実装

次に、Next.js と TypeScript を用いて、イミュータブルなオブジェクトをどのように実装するかについて見ていきましょう。まず、TypeScript でイミュータブルなオブジェクトを表現するためには、readonly 修飾子を使用します。

type ImmutablePerson = {
  readonly name: string
  readonly age: number
}

const person: ImmutablePerson = {
  name: 'John Doe',
  age: 25
}

// 以下のコードはエラーとなる(person のプロパティは変更できない)
// person.name = 'Jane Doe';

上記の例では、ImmutablePerson 型の nameage プロパティは readonly 修飾子により、一度定義されると変更ができないようになります。

ただし、上記の例では配列やネストされたオブジェクトのプロパティは依然として変更可能です。これを防ぐために、TypeScript のユーティリティ型である Readonly を利用することができます。

type MutablePerson = {
  name: string
  age: number
  friends: string[]
}

type ImmutablePerson = Readonly<MutablePerson>

const person: ImmutablePerson = {
  name: 'John Doe',
  age: 25,
  friends: ['Jane', 'Joe']
}

// 以下のコードはエラーとなる(person のプロパティは変更できない)
// person.friends.push('Jack');

Readonly 型は、すべてのプロパティを readonly に変換するため、ネストされたプロパティも含めて変更不能となります。

しかし、これらのテクニックはあくまでTypeScriptのコンパイル時にのみ有効であり、ランタイム時の変更を防ぐことはできません。JavaScriptのランタイムにおけるイミュータビリティを強制するためには、Object.freeze などのメソッドを用いる必要があります。

const person = Object.freeze({
  name: 'John Doe',
  age: 25,
  friends: ['Jane', 'Joe']
});

// 以下のコードはエラーとなる(person のプロパティは変更できない)
// person.friends.push('Jack');

Object.freeze を使用すると、オブジェクトのプロパティを変更する試みはすべて無視され、エラーがスローされます。

以上のように、イミュータビリティは、Next.jsとTypeScriptにおける安全で予測可能なコードを書くための重要な概念です。それはデータの不変性を保証し、予期しない副作用からコードを保護することにより、より堅牢なアプリケーションを作成するのに役立ちます。