TypeScriptのReadonlyを完全解説!10選サンプルコード付き

TypeScriptのReadonlyを図解し、手に持つ本にはサンプルコードが記述されているイメージTypeScript
この記事は約24分で読めます。

 

【サイト内のコードはご自由に個人利用・商用利用いただけます】

この記事では、プログラムの基礎知識を前提に話を進めています。

説明のためのコードや、サンプルコードもありますので、もちろん初心者でも理解できるように表現してあります。

基本的な知識があればカスタムコードを使って機能追加、目的を達成できるように作ってあります。

※この記事は、一般的にプロフェッショナルの指標とされる『実務経験10,000時間以上』を凌駕する現役のプログラマチームによって監修されています。

サイト内のコードを共有する場合は、参照元として引用して下さいますと幸いです

※Japanシーモアは、常に解説内容のわかりやすさや記事の品質に注力しております。不具合、分かりにくい説明や不適切な表現、動かないコードなど気になることがございましたら、記事の品質向上の為にお問い合わせフォームにてご共有いただけますと幸いです。
(送信された情報は、プライバシーポリシーのもと、厳正に取扱い、処分させていただきます。)

はじめに

TypeScriptは、大規模なプロジェクトやチームでの開発において、型の安全性を高めるためのスクリプト言語として、多くの開発者に支持されています。

その中でも、「Readonly」という修飾子は、オブジェクトや配列のプロパティが変更されることを防ぐための重要な役割を果たしています。

この記事では、TypeScriptのReadonlyの役割や基本的な使い方、そしてそれを実際に使ったサンプルコードを初心者にもわかりやすく解説します。

●TypeScriptのReadonlyとは

TypeScriptのReadonlyは、オブジェクトや配列のプロパティを読み取り専用にするための修飾子です。

これを使用すると、そのプロパティの値を後から変更することができなくなります。

JavaScriptには元々このような機能は存在しないため、TypeScriptを使用することで、コードの安全性を高めることができます。

例えば、tugi

のようなオブジェクトがあったとします。

const person = {
    name: "太郎",
    age: 30
};

このオブジェクトのnameageのプロパティを変更することは自由にできます。

しかし、このオブジェクトをReadonlyにすることで、後から値を変更することを禁止することができます。

この機能は、外部からの不要な変更を防ぐためや、関数内でオブジェクトの値を安全に扱いたい場合などに非常に有効です。

○Readonlyの基本的な意味

Readonlyは、その名の通り「読み取り専用」を意味します。

これをオブジェクトや配列の型定義に付与することで、そのプロパティの値を変更することを禁止します。

例を見てみましょう。

type Person = {
    readonly name: string;
    readonly age: number;
};

const taro: Person = {
    name: "太郎",
    age: 30
};

// taro.name = "次郎"; // このコードはエラーになる

このコードでは、Personという型を定義しています。

その中のnameageというプロパティにreadonly修飾子を付与しています。

そのため、taroという変数のnameプロパティを変更しようとすると、TypeScriptのコンパイラによってエラーが出力されます。

また、配列に対しても同様の効果を持ちます。配列自体を読み取り専用にすることができ、後から要素の追加や削除ができなくなります。

●Readonlyの詳細な使い方

TypeScriptには、変数やオブジェクトのプロパティを読み取り専用にするためのReadonlyという特殊な型が提供されています。

この型を使用することで、一度設定された値が後から変更されることを防ぐことができ、コードの品質や安全性を向上させることが期待されます。

では、どのようにしてReadonlyを活用できるのか、詳細に解説していきます。

○サンプルコード1:基本的なReadonlyの利用

まず、最もシンプルなReadonlyの使用例を見てみましょう。

// Readonlyを使用して、読み取り専用のオブジェクトを作成
const obj: Readonly<{ name: string; age: number }> = {
  name: "Taro",
  age: 25,
};

// 以下のコードはエラーになります
// obj.name = "Hanako";  // Error: Cannot assign to 'name' because it is a read-only property.

このコードでは、Readonlyを使ってnameageという2つのプロパティを持つオブジェクトを作成しています。

このオブジェクトには一度値が設定されると、その後で値を変更することはできません。

したがって、obj.name = "Hanako";というコードを実行しようとすると、エラーが発生します。

このコードを実行すると、エラーメッセージが表示され、nameプロパティに値を再代入しようとした行でコードの実行が停止します。

これにより、オブジェクトのプロパティが不意に変更されるリスクを大幅に軽減することができます。

○サンプルコード2:クラスプロパティとしてのReadonly

TypeScriptの開発者として、オブジェクトのプロパティを変更不能にするための強力なツールとしてReadonlyを利用することができます。

特に、クラス内のプロパティが初期化後に変更されてはならない場合に、このReadonlyを利用することが一般的です。

ここでは、クラス内のプロパティをReadonlyとして定義し、それを実際に利用する方法について詳しく解説します。

class User {
    readonly id: number;
    name: string;

    constructor(id: number, name: string) {
        this.id = id;
        this.name = name;
    }
}

const user = new User(1, "Taro");

このコードでは、Userクラスを定義しています。

このクラスには、idというReadonlyのプロパティと、nameという普通のプロパティがあります。

constructor内で、これらのプロパティに初期値を設定しています。

しかし、一度idの値が設定されると、後からその値を変更することはできません。

つまり、次のようなコードはTypeScriptによってエラーとして検出されます。

user.id = 2;  // Error: idはreadonlyなので変更できません。

このコードを実行すると、”idはreadonlyなので変更できません”というエラーメッセージが表示されるでしょう。

一方、nameプロパティはReadonlyを指定していないため、後からその値を自由に変更することができます。

user.name = "Jiro";  // これは問題なく実行できます。

このコードを実行すると、user.nameの値は”Jiro”に更新されます。

○サンプルコード3:Readonlyとジェネリクスの組み合わせ

TypeScriptでは、Readonlyジェネリクスの組み合わせを利用することで、さまざまな型に対して変更不可の属性を追加することができます。

この組み合わせを使うと、再利用性が高いコードを書くことができるため、大規模なアプリケーションやライブラリを作成する際に非常に有用です。

Readonlyジェネリクスの組み合わせを用いたサンプルコードを紹介します。

// ジェネリクスを利用して、任意のオブジェクト型を受け取る関数
function makeReadonly<T>(obj: T): Readonly<T> {
    return Object.freeze(obj);
}

// サンプルのオブジェクト
const person = {
    name: "太郎",
    age: 25
};

// 上記関数を使ってオブジェクトをReadonlyに変換
const readonlyPerson = makeReadonly(person);

このコードでは、makeReadonlyという関数を定義しています。

この関数はジェネリクスを使って、任意のオブジェクト型Tを受け取り、その型のReadonly版を返すように設計されています。

具体的には、関数の中でObject.freezeメソッドを使って、オブジェクトのプロパティの変更を防ぐようにしています。

このコードを実行すると、readonlyPersonオブジェクトはpersonオブジェクトのプロパティを変更することができなくなります。

例えば、readonlyPerson.name = "花子";のようなコードを書くと、コンパイル時にエラーが発生し、その変更は許可されません。

また、このmakeReadonly関数は、さまざまな型のオブジェクトに対して再利用することができます。

例えば、次のような異なる型のオブジェクトでも、この関数を使ってReadonly属性を追加することができます。

const animal = {
    type: "犬",
    name: "ポチ"
};

const readonlyAnimal = makeReadonly(animal);

○サンプルコード4:Readonlyを使った配列の扱い

TypeScriptにおいて、Readonlyはオブジェクトのプロパティを読み取り専用にするためのユーティリティですが、配列に関しても同じ機能を提供しています。

配列の要素を変更不可能にしたい場合、これは非常に役立つ特徴となります。

Readonlyを配列に適用した際のサンプルコードを紹介します。

const numbers: ReadonlyArray<number> = [1, 2, 3, 4, 5];

このコードでは、ReadonlyArray<number>型を使って、数値の読み取り専用の配列を定義しています。

この配列は、後から変更することができないため、次のようなコードを書くとコンパイラエラーが発生します。

numbers[0] = 6; // Error: インデックスシグネチャは読み取り専用です。

このコードを実行すると、配列numbersの先頭の要素を変更しようとしていますが、ReadonlyArrayとして定義されているため、エラーが出力されることがわかります。

また、新しい要素を追加したり、要素を削除したりすることもできません。

numbers.push(6); // Error: 'push' プロパティは 'readonly number[]' 型に存在しません。
numbers.pop();   // Error: 'pop' プロパティは 'readonly number[]' 型に存在しません。

しかし、配列全体を新しい配列で置き換えることは可能です。

const newNumbers = [...numbers, 6]; // [1, 2, 3, 4, 5, 6]

こちらのコードは、スプレッド構文を利用してnumbers配列の要素を新しい配列newNumbersにコピーし、新しい要素6を追加しています。

このような操作を行うことで、元のReadonlyArrayの内容を変更することなく、新しい配列を生成することができます。

○サンプルコード5:Readonlyとオブジェクトのネスト

TypeScriptの中で、Readonlyというユーティリティタイプを利用することで、オブジェクトや配列の要素を変更不可能にすることができます。

これは、データの一貫性や予期せぬ変更から保護することを目的としています。

特に、オブジェクトの中にオブジェクトを持つネストされた構造の場合、その全ての階層でこの保護を適用することが望ましい場合があります。

ここでは、ネストされたオブジェクト構造にReadonlyを適用する方法を詳しく解説します。

サンプルコードをご覧ください。

type NestedObject = {
  firstLevel: {
    secondLevel: {
      thirdLevel: string;
    }
  }
};

const obj: Readonly<NestedObject> = {
  firstLevel: {
    secondLevel: {
      thirdLevel: "Readonlyのテスト"
    }
  }
};

// obj.firstLevel.secondLevel.thirdLevel = "変更しようとする";  // この行はエラーとなる

このコードでは、NestedObjectという型を定義しています。

この型は3階層のネストされたオブジェクト構造を持っています。

そして、Readonly<NestedObject>という型を用いて、このネストされたオブジェクト全体を読み取り専用にしています。

この結果、obj内のどのプロパティも変更することができません。

コメントアウトされている最後の行をコメントアウトを解除して実行しようとすると、TypeScriptはエラーを返し、オブジェクトの内容が変更不可であることを警告します。

このように、Readonlyユーティリティタイプを利用することで、ネストされたオブジェクト構造の各階層を保護することが可能です。

これは、特に大きなプロジェクトや複数の開発者が関与する場合に、データの不整合やバグを未然に防ぐための重要な手段となります。

ただ、この方法には限界も存在します。

Readonlyはシャローコピーのように、直接指定したオブジェクトの一番上の階層だけを保護するのではなく、ネストされたすべてのオブジェクトも含めて読み取り専用にします。

ですが、もしオブジェクト内のオブジェクトが既に変更可能な状態であった場合、その部分のオブジェクトはReadonlyの対象外となります。

●Readonlyの応用例

TypeScriptのReadonlyは非常に強力なツールで、変数やオブジェクトのプロパティが後から変更されないように保証することができます。

基本的な使用方法やその意味を理解した上で、さらに応用的なシチュエーションでの利用方法を探ることで、Readonlyの真の力を引き出すことができます。

○サンプルコード6:条件に基づくReadonlyの使用

考えられるシチュエーションの一つは、特定の条件下でのみオブジェクトをReadonlyにする場合です。

例えば、ユーザーのロールに応じて、あるオブジェクトのプロパティを変更不可にしたいとしましょう。

下記のコードは、ユーザーのロールを判定し、管理者の場合にはオブジェクトを変更可能に、一般ユーザーの場合にはReadonlyにするという処理を行っています。

type User = {
    id: number;
    name: string;
    role: 'admin' | 'user';
};

function getUserData(user: User): user is { role: 'admin' } ? User : Readonly<User> {
    if (user.role === 'admin') {
        return user;
    } else {
        return Object.freeze(user) as Readonly<User>;
    }
}

const userData: User = { id: 1, name: "Taro", role: "user" };
const result = getUserData(userData);

// このコードでは、ユーザーのロールに応じて、オブジェクトを変更可能かReadonlyにするという条件分岐を行っています。

このコードでは、User型を定義し、getUserData関数を通じてユーザーのロールを判定しています。

管理者の場合、オブジェクトはそのまま返されますが、一般ユーザーの場合、Object.freezeを用いてオブジェクトを変更不可にしてから返しています。

このコードを実行すると、userDataが一般ユーザーの場合、resultオブジェクトは変更不可となり、プロパティの変更や追加が行えなくなります。

このような方法で、特定の条件下でのみオブジェクトをReadonlyにすることが可能です。

続いて、このコードの実行結果について考察します。

コードを実行した場合、userDataが管理者であれば、resultは変更可能なオブジェクトとして返されます。

一方、一般ユーザーの場合、resultは変更不可のオブジェクトとして返され、後からそのプロパティを変更することはできません。

○サンプルコード7:関数の引数としてのReadonly

TypeScriptでのプログラム開発では、関数に渡される引数が不意に変更されることを防ぐために、Readonlyを利用することが推奨されています。

関数の中で引数の値を変更しないことを保証するため、Readonlyを使って引数を定義すると、それを明示的に表すことができます。

この方法の利点は、開発者が意図しないデータの変更を防ぐだけでなく、コードを読む他の開発者にも、その引数が関数内で変更されないことを明示的に伝えることができる点です。

具体的なサンプルコードを紹介します。

// この関数では、personオブジェクトを引数として受け取り、その中の名前を表示する処理を行っています。
function displayPersonInfo(person: Readonly<{ name: string, age: number }>) {
  console.log(`名前: ${person.name}`);
  // 以下のコードはエラーとなります。Readonlyにより、personのプロパティは変更不可となっているためです。
  // person.name = "新しい名前";
}

const personData = {
  name: "田中太郎",
  age: 25
};

// 関数を呼び出し
displayPersonInfo(personData);

このコードでは、displayPersonInfo関数はReadonlyを使用して、引数personが変更できないことを明示しています。

そのため、関数内でpersonのプロパティを変更しようとすると、TypeScriptはコンパイルエラーを出します。

このコードを実行すると、コンソールに「名前: 田中太郎」と表示されます。

一方で、関数内でperson.nameの値を変更しようとする行をコメントアウトを外して実行すると、TypeScriptのコンパイラはエラーを報告します。

これは、Readonlyにより、personオブジェクトのプロパティが変更不可であることを表しているためです。

○サンプルコード8:Readonlyと他のユーティリティタイプとの併用

TypeScriptは、強力な型システムを持っており、その中には多くのユーティリティタイプが含まれています。

これらのユーティリティタイプは、より柔軟で高度な型操作を可能にします。

ここでは、Readonlyと他のユーティリティタイプを組み合わせて使用する方法を解説します。

まず、基本的なサンプルコードから始めます。

type UserInfo = {
  name: string;
  age: number;
  address: {
    city: string;
    zip: string;
  };
};

type ReadonlyUserInfo = Readonly<UserInfo>;
type PartialUserInfo = Partial<ReadonlyUserInfo>;

このコードでは、まずUserInfoという型を定義しています。

そして、その型にReadonlyを適用し、その結果をさらにPartialで包んでいます。

このようにして、ユーザーの情報を部分的に持つことが可能な、同時に変更できないオブジェクトを定義することができます。

このコードを実行すると、ReadonlyUserInfoはすべてのプロパティが読み取り専用になり、一方でPartialUserInfoはそのプロパティがオプションとなります。

つまり、PartialUserInfoはいくつかのプロパティを持っていても良く、持っていなくても良いオブジェクトを表しています。

しかしその値は変更することができません。

次に、PickOmitというユーティリティタイプとの組み合わせを見てみましょう。

type UserKeys = 'name' | 'age';
type PickUserInfo = Pick<ReadonlyUserInfo, UserKeys>;
type OmitUserInfo = Omit<ReadonlyUserInfo, UserKeys>;

このコードでは、ReadonlyUserInfoから特定のキーだけを取り出すPickUserInfoと、特定のキーを除外したOmitUserInfoを定義しています。

この二つのユーティリティタイプも、Readonlyと組み合わせて使用することができます。

PickUserInfoは、ReadonlyUserInfoの中からnameageのみを持つ型として定義され、一方でOmitUserInfoはそれらのプロパティを除外した型として定義されます。

このように、必要なプロパティだけを選択したり、不要なプロパティを除外したりすることができます。

○サンプルコード9:再帰的なReadonlyの利用

TypeScriptでのReadonlyの利用には多くの応用例がありますが、特に注目すべきは「再帰的なReadonly」の利用です。

この方法を利用すると、ネストしたオブジェクトに対してもReadonlyを適用することができ、オブジェクトの深い部分まで変更を防ぐことが可能になります。

このコードでは、再帰的にReadonlyを適用するための型を定義しています。

type DeepReadonly<T> = {
    readonly [K in keyof T]: T[K] extends object ? DeepReadonly<T[K]> : T[K];
};

const nestedObject: DeepReadonly<{
    outer: {
        inner: {
            value: string;
        };
        arr: {
            date: Date;
        }[];
    };
}> = {
    outer: {
        inner: {
            value: "hello"
        },
        arr: [{
            date: new Date()
        }]
    }
};

このコードでは、まずDeepReadonlyというジェネリクスの型を定義しています。

この型は、オブジェクトの各プロパティを再帰的にreadonlyに変更します。

それを利用して、nestedObjectという変数にネストしたオブジェクトを定義しています。

このコードを実行すると、nestedObjectのすべてのプロパティとサブプロパティが読み取り専用になるので、後から変更することはできません。

例えば、nestedObject.outer.inner.valueを変更しようとすると、コンパイルエラーが発生します。

しかし、この再帰的なReadonlyの利用は非常に強力な機能であり、大規模なアプリケーションの開発時には特に有用です。

オブジェクトの深い部分に変更が加えられないように保護することで、データの一貫性を保ちやすくなります。

○サンプルコード10:マッピングタイプを活用したReadonlyのカスタマイズ

TypeScriptにはマッピングタイプという機能があります。

これは、既存のタイプを新しいタイプに変換することができる強力なツールです。

マッピングタイプを活用することで、特定の条件に基づいてオブジェクトのプロパティをReadonlyにするなど、柔軟なカスタマイズが可能となります。

ここでは、マッピングタイプを用いて、特定の条件を満たすプロパティだけをReadonlyにするサンプルコードを紹介し、それを解説します。

type CustomReadonly<T> = {
  [K in keyof T]: T[K] extends string ? Readonly<T[K]> : T[K];
};

const obj: CustomReadonly<{ a: string; b: number }> = {
  a: "hello",
  b: 123
};

このコードでは、マッピングタイプを使って「CustomReadonly」という新しいタイプを定義しています。

この「CustomReadonly」は、オブジェクトの各プロパティを調べ、そのプロパティがstring型の場合にはReadonlyを適用し、それ以外の場合はそのままのタイプを使用します。

実際に定義した「CustomReadonly」を使って、オブジェクト「obj」を宣言しています。

この「obj」は、プロパティ「a」がstring型、プロパティ「b」がnumber型となっており、プロパティ「a」のみがReadonlyとして扱われます。

このコードを実行すると、obj.aは変更不可となりますが、obj.bは変更可能となります。

したがって、obj.aへの代入を試みるとTypeScriptのコンパイラエラーが発生しますが、obj.bへの代入は問題なく行えます。

●注意点と対処法

TypeScriptのReadonlyを使用する際、いくつかの注意点やエラーが発生する可能性があります。

これらの注意点とその対処法を詳しく解説していきます。

○Readonlyの適用時のエラーとその対処法

Readonlyを適用した後、変更を試みるとTypeScriptはエラーを出力します。

これは、Readonlyの性質上、一度代入したらその値を変更することができないためです。

例えば、次のようなコードを考えます。

type Person = Readonly<{
  name: string;
  age: number;
}>;

let person: Person = {
  name: "Taro",
  age: 25
};

person.age = 26; // ここでエラーが発生します

このコードでは、PersonというReadonlyな型を定義しています。

そのため、person.age = 26;のように値を変更しようとするとエラーが発生します。

対処法としては、エラーが発生した場所で値の変更をしないようにする、もしくはReadonlyを使用しない型を別途作成して使用する方法が考えられます。

○外部ライブラリや既存のコードとの統合時の注意点

Readonlyを使って定義された型やインターフェースを外部ライブラリや既存のコードと統合する際にも注意が必要です。

外部ライブラリなどで、Readonlyを前提としていない関数やメソッドにReadonlyなオブジェクトを渡すと、エラーや予期せぬ動作が発生する可能性があります。

例として、次のようなコードを考えます。

type Person = Readonly<{
  name: string;
  age: number;
}>;

let person: Person = {
  name: "Taro",
  age: 25
};

function updateAge(p: { name: string; age: number }, newAge: number) {
  p.age = newAge;
}

updateAge(person, 26); // ここでエラーが発生します

このコードでは、updateAge関数はReadonlyを前提としていないため、Readonlyなpersonオブジェクトを渡すとエラーが発生します。

対処法としては、関数内でオブジェクトのコピーを作成して操作する、または関数の引数の型をReadonlyを前提としたものに変更するなどの方法が考えられます。

●Readonlyのカスタマイズ方法

TypeScriptのReadonlyは非常に便利なユーティリティタイプですが、特定の状況や要件に応じてカスタマイズすることも可能です。

ここでは、Readonlyをカスタマイズする方法と、その具体的な実装例について解説します。

○カスタマイズ可能な点とその実装例

□カスタムReadonlyの作成

Readonlyの本質的な役割は、オブジェクトのプロパティを変更不可にすることです。

この動作をカスタマイズして、特定のプロパティだけを変更不可にするカスタムReadonlyを作成することができます。

type CustomReadonly<T> = {
  readonly name?: T['name'];
  age: T['age'];
};

const user: CustomReadonly<{ name: string; age: number }> = {
  name: "太郎",
  age: 20
};

このコードを使用することで、特定のプロパティだけをReadonlyにするカスタムユーティリティタイプを定義することができます。

この例では、nameプロパティだけがReadonlyとなり、他のプロパティは変更可能としています。

□条件を基づくカスタムReadonlyの実装

特定の条件を満たす場合のみ、プロパティをReadonlyにするカスタマイズも可能です。

例えば、プロパティの値が特定の値の場合にだけ、そのプロパティをReadonlyにしたい場合などに役立ちます。

type ConditionalReadonly<T> = T['value'] extends 'fixed' ? { readonly value: 'fixed' } : T;

const data1: ConditionalReadonly<{ value: 'fixed' }> = {
  value: 'fixed'
};

const data2: ConditionalReadonly<{ value: 'dynamic' }> = {
  value: 'dynamic'
};

この例では、valueプロパティが’fixed’の場合のみReadonlyにし、それ以外の場合は変更可能としています。

このように、条件を基づくカスタムReadonlyの実装を行うことで、より柔軟にReadonlyの振る舞いをカスタマイズすることができます。

まとめ

TypeScriptにおけるReadonlyは、オブジェクトや配列の要素を読み取り専用にするための強力なツールです。

この記事では、Readonlyの基本的な意味から、さまざまな応用例や注意点、カスタマイズ方法まで、実際のサンプルコードを交えて徹底的に解説しました。

全体を通して、TypeScriptのReadonlyは、コードの品質や保守性を向上させるための有効な手段と言えるでしょう。

この記事を参考に、自分のプロジェクトでのReadonlyの利用法をより深く理解し、効果的に活用していきましょう。