下記のようなUnion Typeからname
propertyだけ取り除いた
OmittedType
を作りたいとします。
type Type = {
name: string,
type: 1,
prop1: string,
} | {
name: string,
type: 2,
prop2: string,
}
/** 下記のような型を作りたい
type OmittedType = {
type: 1,
prop1: string,
} | {
type: 2,
prop2: string,
}
**/
この時素直にOmitすると
type OmittedType = Omit<Type, "name"> // = {type: 1 | 2;}
となってしまいprop1
prop2
のプロパティが消えてしまい
うまくいきません。
なぜこうなるのでしょうか?
Omitの実装は、下記のようになっています。
type Omit<T, K extends keyof any> = Pick<T, Exclude<keyof T, K>>;
渡された型Tのキーを除いたものをPickするという、シンプルな実装に見えます。
そこで順番に確認してみます。
type KeyType = keyof Type
KeyType
は"name" | "type" | "prop1" | "prop2"
になって欲しいですが、実際は"name" | "type"
になります。これはTypeScriptの仕様として、keyofの対象がUnionだった場合、その中の共通して存在するキーだけが取り出されるためです。(https://github.com/microsoft/TypeScript/issues/12948)
そのため、Omit<Type, "name">
の型は内部的にはPick<Type, Exclude<"name"|"type", "name">>
となり
Type
からtype
プロパティだけを取り出すことになるので {type: 1 | 2;}
となってしまうわけです。
解決策
Conditional Typesを用いて次のような型を作成すれば、うまくUnion Typeのプロパティに対してOmitすることができます。
type TheOmit<T, K extends keyof T> = T extends T ? Omit<T, K> : never
type NewOmittedType = TheOmit<Type, "name">
/**
型補完は次のようになる
type NewOmittedType = Omit<{
name: string;
type: 1;
prop1: string;
}, "name"> | Omit<{
name: string;
type: 2;
prop2: string;
}, "name">
**/
これはUnion Distributionをうまく使ったものです。
Union DistributionはUnionが型変数であるとき起こる特殊な挙動です。
例えば、下記のような実装をしたときBoxedValue
はどんな型になっているでしょうか?
type BoxedValue<T> = { value: T };
type DistributedBoxedValue = BoxedValue<string | number>;
普通に考えると、{ value: string | number }
になりそうなのですが
{ value: string } | { value: number }
という型になります。
これがUnion Distributionの挙動で、Unionの分配が起きると、
BoxedValue<string> | BoxedValue<number>
のように
型変数が分配されてから型の計算が行われます。
ここでもう一度TheOmit
の実装を見ています。
type TheOmit<T, K extends keyof T> = T extends T ? Omit<T, K> : never
下記で内部に起こっていることを順を追って説明します。
type Type = {
name: string,
type: 1,
prop1: string,
} | {
name: string,
type: 2,
prop2: string,
}
type TheOmit<Type, "name">
Type
の型を展開
type TheOmit<{
name: string,
type: 1,
prop1: string,
} | {
name: string,
type: 2,
prop2: string,
}, "name">
Union Distributionにより型が分配される
TheOmit<{
name: string,
type: 1,
prop1: string,
} , "name"> | TheOmit<{
name: string,
type: 2,
prop2: string,
} , "name">
TheOmit
の中身に当てはめる(片側のみ)
{
name: string,
type: 1,
prop1: string,
} extends {
name: string,
type: 1,
prop1: string,
} ? Omit<{
name: string,
type: 1,
prop1: string,
}, "name"> : never
もちろんextendsはtrueなので
Omit<{
name: string,
type: 1,
prop1: string,
}, "name">
もう片方も同様に
Omit<{
name: string;
type: 2;
prop2: string;
}, "name">
よってTheOmit<Type, "name">
は
Omit<{
name: string;
type: 1;
prop1: string;
}, "name"> | Omit<{
name: string;
type: 2;
prop2: string;
}, "name">
という型になります。
参考