メインコンテンツまでスキップ

Conditional Types

Conditional Typesはちょうどプログラミング言語の三項演算子のように?:を使ってT extends U ? X : Yのように書きます。これはTUに割り当て可能である場合、Xになり、そうでない場合はYになります。

たとえば、あるobject型のプロパティを読み取り専用にするReadonly<T>というユーティリティ型があります。Readonly<T>はそのオブジェクトの直下のプロパティを読み取り専用にしますが、ネストしたオブジェクトのプロパティは読み取り専用にしません。たとえば、次のようなオブジェクトがあるとします。

ts
type Person = {
name: string;
age: number;
address: {
country: string;
city: string;
};
};
ts
type Person = {
name: string;
age: number;
address: {
country: string;
city: string;
};
};

このときReadonly<Person>ではaddressのプロパティのcountrycityは読み取り専用になっていません。上書きが可能です。

ts
const kimberley: Readonly<Person> = {
name: "Kimberley",
age: 24,
address: {
country: "Canada",
city: "Vancouver",
},
};
 
kimberley.name = "Kim";
Cannot assign to 'name' because it is a read-only property.2540Cannot assign to 'name' because it is a read-only property.
kimberley.age = 25;
Cannot assign to 'age' because it is a read-only property.2540Cannot assign to 'age' because it is a read-only property.
kimberley.address.country = "United States";
kimberley.address.city = "Seattle";
ts
const kimberley: Readonly<Person> = {
name: "Kimberley",
age: 24,
address: {
country: "Canada",
city: "Vancouver",
},
};
 
kimberley.name = "Kim";
Cannot assign to 'name' because it is a read-only property.2540Cannot assign to 'name' because it is a read-only property.
kimberley.age = 25;
Cannot assign to 'age' because it is a read-only property.2540Cannot assign to 'age' because it is a read-only property.
kimberley.address.country = "United States";
kimberley.address.city = "Seattle";

このようにaddress.countryaddress.cityは書き換え可能です。これを解決するにはReadonly<T>を再帰的に適用する必要があります。このような場合にMapped TypesとConditional Typesを組み合わせて使います。

ts
type Freeze<T> = Readonly<{
[P in keyof T]: T[P] extends object ? Freeze<T[P]> : T[P];
}>;
ts
type Freeze<T> = Readonly<{
[P in keyof T]: T[P] extends object ? Freeze<T[P]> : T[P];
}>;

このようなFreeze<T>を作ってみました。まずはこれを使ってみましょう。

ts
const kimberley: Freeze<Person> = {
name: "Kimberley",
age: 24,
address: {
country: "Canada",
city: "Vancouver",
},
};
 
kimberley.name = "Kim";
Cannot assign to 'name' because it is a read-only property.2540Cannot assign to 'name' because it is a read-only property.
kimberley.age = 25;
Cannot assign to 'age' because it is a read-only property.2540Cannot assign to 'age' because it is a read-only property.
kimberley.address.country = "United States";
Cannot assign to 'country' because it is a read-only property.2540Cannot assign to 'country' because it is a read-only property.
kimberley.address.city = "Seattle";
Cannot assign to 'city' because it is a read-only property.2540Cannot assign to 'city' because it is a read-only property.
ts
const kimberley: Freeze<Person> = {
name: "Kimberley",
age: 24,
address: {
country: "Canada",
city: "Vancouver",
},
};
 
kimberley.name = "Kim";
Cannot assign to 'name' because it is a read-only property.2540Cannot assign to 'name' because it is a read-only property.
kimberley.age = 25;
Cannot assign to 'age' because it is a read-only property.2540Cannot assign to 'age' because it is a read-only property.
kimberley.address.country = "United States";
Cannot assign to 'country' because it is a read-only property.2540Cannot assign to 'country' because it is a read-only property.
kimberley.address.city = "Seattle";
Cannot assign to 'city' because it is a read-only property.2540Cannot assign to 'city' because it is a read-only property.

Readonly<T>とは異なり、address.countryaddress.cityが書き換え不可能になりました。これはFreeze<T>が再帰的に適用されているからです。

[P in keyof T]の部分についてはMapped Typesのページで説明していますのでここでは簡潔に説明します。keyof Tはオブジェクトのキーをユニオン型に変更するものです。kimberleyの場合は"name" | "age" | "address"になります。inはその中のどれかを意味します。
T[P]でオブジェクトのあるキーにおけるプロパティの型を取得します。その型がobjectであれば再起的にFreeze<T[P]>を適用し、そうでなければT[P]をそのまま使います。

📄️ Mapped Types

インデックス型では設定時はどのようなキーも自由に設定できてしまい、アクセス時は毎回undefinedかどうかの型チェックが必要です。入力の形式が決まっているのであればMapped Typesの使用を検討できます。

これによってオブジェクトを再帰的に凍結することができました。