Conditional Types
Conditional Typesはちょうどプログラミング言語の三項演算子のように?
と:
を使ってT extends U ? X : Y
のように書きます。これはT
がU
に割り当て可能である場合、X
になり、そうでない場合はY
になります。
たとえば、あるobject型のプロパティを読み取り専用にするReadonly<T>
というユーティリティ型があります。Readonly<T>
はそのオブジェクトの直下のプロパティを読み取り専用にしますが、ネストしたオブジェクトのプロパティは読み取り専用にしません。たとえば、次のようなオブジェクトがあるとします。
ts
typePerson = {name : string;age : number;address : {country : string;city : string;};};
ts
typePerson = {name : string;age : number;address : {country : string;city : string;};};
このときReadonly<Person>
ではaddress
のプロパティのcountry
とcity
は読み取り専用になっていません。上書きが可能です。
ts
constkimberley :Readonly <Person > = {name : "Kimberley",age : 24,address : {country : "Canada",city : "Vancouver",},};Cannot assign to 'name' because it is a read-only property.2540Cannot assign to 'name' because it is a read-only property.kimberley .= "Kim"; name Cannot assign to 'age' because it is a read-only property.2540Cannot assign to 'age' because it is a read-only property.kimberley .= 25; age kimberley .address .country = "United States";kimberley .address .city = "Seattle";
ts
constkimberley :Readonly <Person > = {name : "Kimberley",age : 24,address : {country : "Canada",city : "Vancouver",},};Cannot assign to 'name' because it is a read-only property.2540Cannot assign to 'name' because it is a read-only property.kimberley .= "Kim"; name Cannot assign to 'age' because it is a read-only property.2540Cannot assign to 'age' because it is a read-only property.kimberley .= 25; age kimberley .address .country = "United States";kimberley .address .city = "Seattle";
このようにaddress.country
とaddress.city
は書き換え可能です。これを解決するにはReadonly<T>
を再帰的に適用する必要があります。このような場合にMapped TypesとConditional Typesを組み合わせて使います。
ts
typeFreeze <T > =Readonly <{[P in keyofT ]:T [P ] extends object ?Freeze <T [P ]> :T [P ];}>;
ts
typeFreeze <T > =Readonly <{[P in keyofT ]:T [P ] extends object ?Freeze <T [P ]> :T [P ];}>;
このようなFreeze<T>
を作ってみました。まずはこれを使ってみましょう。
ts
constkimberley :Freeze <Person > = {name : "Kimberley",age : 24,address : {country : "Canada",city : "Vancouver",},};Cannot assign to 'name' because it is a read-only property.2540Cannot assign to 'name' because it is a read-only property.kimberley .= "Kim"; name Cannot assign to 'age' because it is a read-only property.2540Cannot assign to 'age' because it is a read-only property.kimberley .= 25; age Cannot assign to 'country' because it is a read-only property.2540Cannot assign to 'country' because it is a read-only property.kimberley .address .= "United States"; country Cannot assign to 'city' because it is a read-only property.2540Cannot assign to 'city' because it is a read-only property.kimberley .address .= "Seattle"; city
ts
constkimberley :Freeze <Person > = {name : "Kimberley",age : 24,address : {country : "Canada",city : "Vancouver",},};Cannot assign to 'name' because it is a read-only property.2540Cannot assign to 'name' because it is a read-only property.kimberley .= "Kim"; name Cannot assign to 'age' because it is a read-only property.2540Cannot assign to 'age' because it is a read-only property.kimberley .= 25; age Cannot assign to 'country' because it is a read-only property.2540Cannot assign to 'country' because it is a read-only property.kimberley .address .= "United States"; country Cannot assign to 'city' because it is a read-only property.2540Cannot assign to 'city' because it is a read-only property.kimberley .address .= "Seattle"; city
Readonly<T>
とは異なり、address.country
とaddress.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の使用を検討できます。
これによってオブジェクトを再帰的に凍結することができました。