Omit<Type, Keys>が(期待どおりに)機能しないとき
2022-07-12
TypeScriptのOmit<Type, Keys>
が意図したとおりに機能せず頭を抱えてしまいました。
このブログ投稿はOmit
が機能しない理由を考えることで学んだことを共有します。
Omit<Type, Keys>とは?
Omit<Types, Keys>
はTypeScriptが提供するユーティリティ型です。
Type
のすべてのプロパティをすべて持つがKeys
に指定したプロパティは欠く新しい型を定義します。
Keys
をType
から削除する型演算子と見ることができます。
以下の定義があるとすると、
;
導出されたPerson
は以下と等価です。
Omit<Type, Keys>が機能しないとき
自分のプロジェクト外に以下のような型が定義されていると想定します。
InfoObject
のtitle
プロパティをオプションにしたいとします。
そこでInfoObject
からOmit
を使って新しい型を導出します。
;
これでInfoObjectOmittingTitle
のインスタンスを初期化する際にtitle
プロパティを省略することができます。
; // エラー! error TS2741: Property 'title' is missing in type '{ version: string; }' but required in type 'InfoObject'.
; // OK!
しかしおかしなことが起こります。
期待に反してversion
プロパティもオプションになっているのです。
; // OKなの!?
さらには、以下ですらコンパイルできてしまいます!
; // OKなの!?
Omit<Type, Keys>はどのように機能するか
Omit
自体の定義はTypeScriptソースコードのこちらにあります。
以下はコードの抜粋です。
;
Omit
は実際のところPick
とExclude
という別のユーティリティ型を組み合わせたものです。
Pick<Type, Keys>とは?
Pick
はOmit
の反対です。
Type
のプロパティでKeys
に含まれるものを持つ新しい型を定義します。
Pick
自体の定義はこちらにあります。
以下はコードの抜粋です。
;
Exclude<UnionType, ExcludedMembers>とは?
Exclude
はUnionType
のすべてのメンバーを持つがExcludedMembers
に含まれるものは除外する新しいユニオン型を定義します。
Exclude
自体の定義はこちらにあります。
以下はコードの抜粋です。
;
私の失敗例では何が起きているのか?
前の節の以下の部分を詳しく分解していきます。
Omit<InfoObject, 'title'>
これは以下のようになります。
Pick<InfoObject, Exclude<keyof InfoObject, 'title'>>
keyof InfoObject
は以下のユニオン型と等価です。
keyof
がどのように機能するかについてはこちらのページを参照ください。
'title' | 'version' | 'description' | string;
string
はベースインターフェイスIElement
のIndex Signature [extensionName: string]: any
からきています。
ということでOmit
部分はさらに以下のようになります。
Pick<InfoObject, Exclude<'title' | 'version' | 'description' | string, 'title'>>
以下のテーブルは'title' | 'version' | 'description' | string
の各メンバーT
についてExclude<T, 'title'>
を評価した結果を示しています。
Exclude<'title' | 'version' | 'description' | string, 'title'>
の部分で何が起きているのかを知るには"Distributive Conditional Types"を理解しなければなりません。
T | T extends 'title' ? | Exclude<T, 'title'> |
---|---|---|
'title' | true | never |
'version' | false | 'version' |
'description' | false | 'description' |
string | false | string |
つまりExclude<'title' | 'version' | 'description' | string, 'title'>
は以下のようになります。
'version' | 'description' | string
そしてOmit
部分は以下のようになります。
Pick<InfoObject, 'version' | 'description' | string>
最終的なPick
では何が起きるでしょうか?
ここは結果から推測することしかできません。
Pick<T, K>
の展開の際にK extends keyof T
もしくはP in K
で最も包括的なstring
が優先し、より限定的なメンバーの'version'
と'description'
が抑え込まれるのではないかと思われます。
結果的にPick
は以下のようになります。
しかし、私の推測をバックアップする根拠となる文献はまだ確認できていません。
回避策
こちらの記事によると、以下が回避策になりそうです。
;
Partial<Type>
はType
のすべてのプロパティをオプションとする新しい型を定義します。
Pick<InfoObject, 'version'>
とのインターセクションによりversion
プロパティが必須のままになります。
主な難点は必須プロパティを残さず列挙しなければならないことです。
結論
Omit<Type, Keys>
はType
がIndex Signatureを含むと機能しないかもしれません。
なぜこれが起きるのかTypeScriptの仕様からは確認できていません。
しかし回避策は見つかりました。