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

Conditional Types

Conditional Typesは日本語では条件付き型、型の条件分岐、条件型などと呼ばれ、ちょうど三項演算子のように?:を使ってT extends U ? X : Yのように書きます。これはTUに割り当て可能である場合、Xになり、そうでない場合はYになります。

このような場合だと型はtrueになります。

ts
type IsString<T> = T extends string ? true : false;
 
const a: IsString<"a"> = true;
const a: true
ts
type IsString<T> = T extends string ? true : false;
 
const a: IsString<"a"> = true;
const a: true

たとえば、ある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プロパティ自体は読み取り専用になっており書き換えることはできませんが、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 = {
Cannot assign to 'address' because it is a read-only property.2540Cannot assign to 'address' because it is a read-only property.
country: "United States",
city: "Seattle",
};
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 = {
Cannot assign to 'address' because it is a read-only property.2540Cannot assign to 'address' because it is a read-only property.
country: "United States",
city: "Seattle",
};
kimberley.address.country = "United States";
kimberley.address.city = "Seattle";

これを解決するには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 = {
Cannot assign to 'address' because it is a read-only property.2540Cannot assign to 'address' because it is a read-only property.
country: "United States",
city: "Seattle",
};
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 = {
Cannot assign to 'address' because it is a read-only property.2540Cannot assign to 'address' because it is a read-only property.
country: "United States",
city: "Seattle",
};
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の使用を検討できます。

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