TypeScriptにおけるミュータブルとイミュータブル

TypeScriptにおけるミュータブルとイミュータブル

この記事ではTypeScriptにおけるミュータブルとイミュータブルについて説明します。

YouTube Video

TypeScriptにおけるミュータブルとイミュータブル

ミュータブルとは?

ミュータブル(mutable) とは、値を変更できる ことを意味します。オブジェクトや配列などの 参照型(reference type)は、ミュータブルなデータ構造の代表例です。

オブジェクトのミュータブルな例

1type Person = { name: string; age: number };
2
3// Mutable Example: Object
4let person: Person = { name: "Alice", age: 25 };
5person.age = 26;
6console.log(person); // { name: "Alice", age: 26 }

このコードでは、person オブジェクトの age プロパティを 25 から 26 に変更しています。オブジェクトは 参照渡し されるため、変数 person に格納された メモリアドレス の指す内容が変更されます。

配列のミュータブルな例

1// Mutable Example: Array
2let numbers: number[] = [1, 2, 3];
3numbers.push(4);
4console.log(numbers); // [1, 2, 3, 4]

このコードでは push メソッドを使って、元の配列に新しい要素 4 を追加しています。これは 元の配列を変更 しており、ミュータブルな操作です。

関数の例

1// Mutable Example: Function
2function append(arr: number[], value: number): void {
3    arr.push(value); // Modify the original array
4    console.log(arr);
5}
6
7let nums: number[] = [1, 2, 3];
8append(nums, 4);
9console.log(nums); // [1, 2, 3, 4]

このようにミュータブルな操作を関数内で行った場合にも、元の配列が変更されます。

イミュータブルとは?

イミュータブル(immutable) とは、値を変更できない ことを意味します。プリミティブ型(primitive type) は基本的にイミュータブルです。

プリミティブ型のイミュータブルな例

1// Immutable Example: String
2let str: string = "hello";
3str[0] = "H"; // Error: Index assignment is not allowed
4console.log(str); // "hello"

文字列 str の一文字目を H に変更しようとしていますが、文字列は イミュータブル であるため変更されません。

関数の例

1// Immutable Example: Function
2function increment(num: number): number {
3    num++; // This modifies only the local copy of num
4    return num;
5}
6
7let number: number = 10;
8console.log(increment(number)); // 11
9console.log(number); // 10 (original number remains unchanged)

このように数値はイミュータブルであるため、関数内での操作は元の変数に影響しません。

配列のイミュータブルな操作

配列はミュータブルですが、元の配列を変更せずに新しい配列を作成する ことでイミュータブルな操作を実現できます。

1// Create an array of numbers
2let numbers: number[] = [1, 2, 3];
3
4// Immutable Example: Creating a new array
5let newNumbers: number[] = [...numbers, 4];
6
7console.log(numbers); // [1, 2, 3] (original array is unchanged)
8console.log(newNumbers); // [1, 2, 3, 4] (new array with an added element)

ここではスプレッド構文(...)を用いて、新しい配列 newNumbers を作成しています。元の numbers は変更されていないため、イミュータブルな操作です。

イミュータブルデータ構造を活用するメリット

予測可能性の向上

イミュータブルなデータは変更されないため、予期しない変更が発生しづらく、バグの発生を防げます。

不変性を前提としたライブラリとの相性

ReactRedux などのライブラリでは、イミュータブルなデータを前提とした設計が多く、適切に利用することで状態管理が容易になります。

Object.freeze によるオブジェクトのイミュータブル化

Object.freeze を使用すると、オブジェクトの変更を防ぐことができます。

1// Create a frozen object (properties cannot be modified)
2const person = Object.freeze({ name: "Alice", age: 25 });
3
4// Attempt to modify a property (ignored in non-strict mode, error in strict mode)
5person.age = 26;
6
7console.log(person); // { name: "Alice", age: 25 }

ただし、Object.freeze浅い凍結(shallow freeze) であり、ネストされたオブジェクトのプロパティは変更可能なままです。

1// Create a frozen object with a nested object
2const user: Readonly<{ profile: { name: string } }> = Object.freeze({
3    profile: { name: "Bob" }
4});
5
6// Attempt to modify a nested property (this works because Object.freeze() is shallow)
7user.profile.name = "Charlie"; // No TypeScript error, but still mutable
8
9console.log(user.profile.name); // "Charlie" (nested object is still mutable)

完全にイミュータブルなオブジェクトを作るには、深い凍結(deep freeze)が必要です。

 1// Function to deeply freeze an object, making all nested objects immutable
 2function deepFreeze<T>(obj: T): Readonly<T> {
 3    Object.freeze(obj);
 4    Object.getOwnPropertyNames(obj).forEach(prop => {
 5        const value = (obj as any)[prop];
 6        if (typeof value === "object" && value !== null) {
 7            deepFreeze(value);
 8        }
 9    });
10    return obj;
11}
12
13// Create a deeply frozen object
14const user = deepFreeze({
15  profile: { name: "Bob" }
16});
17
18// Attempt to modify a nested property
19// (this will now throw an error in strict mode)
20user.profile.name = "Charlie";  // No TypeScript error, but modification is not allowed at runtime
21
22console.log(user.profile.name); // "Bob" (unchanged due to deep freeze)

まとめ

  • ミュータブル(mutable) なデータは変更可能で、オブジェクトや配列が該当します。
  • イミュータブル(immutable) なデータは変更不可能で、プリミティブ型(文字列、数値など)が該当します。
  • スプレッド構文や map などを活用すると、イミュータブルなデータ操作が可能 になります。
  • Object.freezedeepFreeze を使うと、オブジェクトの変更を防げます。
  • イミュータブルなデータを使うことで、予測しやすくバグの少ないコードを記述できます。

イミュータブルな設計は、コードの安全性と可読性を向上させるため、積極的に活用しましょう!

YouTubeチャンネルでは、Visual Studio Codeを用いて上記の記事を見ながら確認できます。 ぜひYouTubeチャンネルもご覧ください。

YouTube Video