mosya
mosya Business はこちら

mosya<TC> - オブジェクト型のすべてのプロパティを読み取り専用にする型

この記事はmosya<TC>の問題の一つであるDeepReadonly型の解説になります。

問題

オブジェクトのすべてのパラメーター(およびそのサブオブジェクトを再帰的に)を読み取り専用にするDeepReadonlyを実装します。

この課題ではオブジェクトのみを扱っていると想定してください。配列、関数、クラスなどは考慮する必要はありません。

以下のようなコードを満たすようにDeepReadonlyを実装しましょう。

type X = {
  x: {
    a: 1;
    b: "hi";
  };
  y: "hey";
};

type Expected = {
  readonly x: {
    readonly a: 1;
    readonly b: "hi";
  };
  readonly y: "hey";
};

type Todo = DeepReadonly<X>; // should be same as `Expected`

前提知識

この問題を解くにあたって型についての以下の知識を理解しておく必要があります。

  1. Conditional Typesを理解する
  2. Mapped Typesを理解する
  3. readonlyを理解する
  4. 再帰的な型を理解する

Conditional Typesを理解する

Conditional Typesは、条件によって型を変更することができる機能です。

例えば、以下のような型が考えられます。

type Foo<T> = T extends string
  ? string
  : number;

この型は、Tstring型を継承している場合はstring型を、そうでない場合はnumber型を返す型です。

このように、extendsを使って条件を指定することで、条件によって型を変更することができます。

Mapped Typesを理解する

Mapped Typesはユニオン型を使って、新しいオブジェクトの型を生成する機能です。

例えば、以下のようなユニオン型があったとします。

type TodoKeys = "title" | "description";

このユニオン型を使って、以下のようなオブジェクトの型を生成することができます。

type Todo = {
  [K in TodoKeys]: string;
};

// 以下のように展開される
type Todo = {
  title: string;
  description: string;
};

インデックスアクセス型を理解する

インデックスアクセス型は、オブジェクトのプロパティにアクセスするための機能です。

例えば、以下のようなオブジェクトがあったとします。

interface Todo {
  title: string;
  description: string;
}

このオブジェクトのプロパティにアクセスするには、以下のようにします。

type Title = Todo["title"]; // string

このように、Todo["title"]とすることで、Todotitleプロパティの型を取得することができます。
これをインデックスアクセス型と呼びます。

先ほど登場したMapped Typesと組み合わせることで、一つずつオブジェクトのプロパティにアクセスすることができます。

readonly修飾子を理解する

readonly修飾子は、プロパティを読み取り専用にする修飾子です。
この修飾子があると、プロパティの再割り当てができなくなります。

interface Todo {
  readonly title: string;
  description: string;
}

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

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

再帰的な型を理解する

再帰的な型は、自分自身を参照する型のことです。
例えば、以下のような型が考えられます。

type Tree<T> = {
  value: T;
  left: Tree<T>;
  right: Tree<T>;
};

この型は、Treeという型がvalueというプロパティを持ち、leftrightというプロパティはTree型を持つという型です。

以下のような使用例が考えられます。

// 使用例
const node: Tree = {
  value: 1,
  left: {
    value: 2,
    left: null,
    right: null,
  },
  right: {
    value: 3,
    left: null,
    right: {
      value: 4,
      left: null,
      right: null,
    },
  },
};

このように、再帰的な型は繰り返し同じ型を再利用したい場合に便利です。

解答例

以上の知識を使って、以下のように解答することができます。

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

この解答例では、Conditional TypesMapped Typesを使って、再帰的に型を表現しています。

例えば、以下のような型があったとします。

type X = {
  x: {
    a: 1;
    b: "hi";
  };
  y: "hey";
};

この時、X[x]{ a: 1, b: 'hi' }というオブジェクト型なので、extendsを使った条件分岐で再度DeepReadonlyが適用され、{ readonly a: 1, readonly b: 'hi' }という型になります。

Authored by

筆者の写真

Godai@steelydylan

Webサービスを作るのが好きなWebエンジニア。子供が産まれたことをきっかけに独立し法人化。サービス開発が大好き。
好きな言語はTypeScript。

ReactやTypeScriptなどの周辺技術が学べる
オンライン学習サービスを作りました!

詳しくはこちら
mosya

mosyaはオンラインでHTML,CSS,JavaScriptを基本から学習できるサービスです。現役エンジニアが作成した豊富なカリキュラムに沿って学習を進めましょう。

© 2023 - mosya. All rights reserved.