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`
前提知識
この問題を解くにあたって型についての以下の知識を理解しておく必要があります。
Conditional Types
を理解するMapped Types
を理解するreadonly
を理解する- 再帰的な型を理解する
Conditional Typesを理解する
Conditional Typesは、条件によって型を変更することができる機能です。
例えば、以下のような型が考えられます。
type Foo<T> = T extends string
? string
: number;
この型は、T
がstring
型を継承している場合は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"]
とすることで、Todo
のtitle
プロパティの型を取得することができます。
これをインデックスアクセス型と呼びます。
先ほど登場した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
というプロパティを持ち、left
とright
というプロパティは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 Types
とMapped 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。