🔒

【type-challengesに挑戦!】Readonly の解答と解説

に公開

はじめに

この記事は、type-challenges の問題を解説するシリーズです。今回は Readonly の問題に挑戦します。

問題

TypeScript の組み込みユーティリティ型 Readonly<T> を使用せずに実装します。

Readonly<T> は、型 T のすべてのプロパティを読み取り専用に設定した型を構築します。つまり、構築された型のプロパティは再代入できなくなります。

例:

interface Todo {
  title: string
  description: string
}

const todo: MyReadonly<Todo> = {
  title: "Hey",
  description: "foobar"
}

todo.title = "Hello" // Error: cannot reassign a readonly property
todo.description = "barFoo" // Error: cannot reassign a readonly property

解答

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

解説

ステップ 1: 型パラメータの定義

type MyReadonly<T>

1 つの型パラメータを定義しています:

  • T: 元となる型(例: Todo インターフェース)

ステップ 2: マップド型 [P in keyof T]

{
  [P in keyof T]: T[P];
}

この構文はマップド型と呼ばれ、以下の処理を行います:

  1. keyof T: 型 T のすべてのプロパティ名をユニオン型として取得
    • 例: keyof Todo"title" | "description"
  2. [P in keyof T]: keyof T に含まれる各プロパティ名を P として反復処理
  3. T[P]: 元の型 T から、プロパティ P の型を取得(インデックスアクセス型)

具体例で見てみましょう

type ReadonlyTodo = MyReadonly<Todo>;

この場合、以下のように展開されます:

  1. keyof Todo"title" | "description"
  2. P in "title" | "description"P が順に "title", "description" になる
  3. それぞれの型を取得:
    • Todo["title"]string
    • Todo["description"]string
  4. 結果: { title: string; description: string; }

ステップ 3: readonly 修飾子

{
  readonly [P in keyof T]: T[P];
}

ここが MyReadonly の最も重要な部分です:

  • readonly: プロパティを読み取り専用として宣言します
  • マップド型の各プロパティに readonly 修飾子を追加することで、すべてのプロパティが読み取り専用になります

動作の確認

type ReadonlyTodo = MyReadonly<Todo>;
// 結果: { readonly title: string; readonly description: string; }

const todo: ReadonlyTodo = {
  title: "Hey",
  description: "foobar"
};

// ✅ 読み取りは可能
console.log(todo.title); // "Hey"

// ❌ エラー: 再代入はできない
todo.title = "Hello";
// Cannot assign to 'title' because it is a read-only property.

todo.description = "barFoo";
// Cannot assign to 'description' because it is a read-only property.

readonly 修飾子の効果

readonly 修飾子は、コンパイル時にプロパティへの再代入を防ぎます:

interface Person {
  name: string;
  age: number;
}

type ReadonlyPerson = MyReadonly<Person>;
// { readonly name: string; readonly age: number; }

const person: ReadonlyPerson = {
  name: "Alice",
  age: 30
};

// ✅ OK: オブジェクトの作成時は値を設定できる
const anotherPerson: ReadonlyPerson = {
  name: "Bob",
  age: 25
};

// ✅ OK: 読み取りは自由にできる
console.log(person.name); // "Alice"
console.log(person.age);  // 30

// ❌ エラー: 作成後の再代入はできない
person.name = "Carol";
// Cannot assign to 'name' because it is a read-only property.

person.age = 31;
// Cannot assign to 'age' because it is a read-only property.

まとめ

この記事では、type-challenges の Readonly 問題を通じて、以下の TypeScript の重要な概念を学びました:

  1. マップド型: 既存の型からプロパティを反復的に変換
  2. keyof 演算子: 型のプロパティ名をユニオン型として取得
  3. readonly 修飾子: プロパティを読み取り専用にする
  4. インデックスアクセス型 (T[P]): 型からプロパティの型を取得
  5. イミュータブルな型の作成: オブジェクトの不変性を型レベルで保証

readonly 修飾子をマップド型と組み合わせることで、既存の型のすべてのプロパティを一度に読み取り専用に変換できます。これにより、コンパイル時に意図しない変更を防ぐことができ、より安全なコードを書くことができます。

参考リンク

Discussion