3
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Union Typeのプロパティに対してOmitするとうまくいかない問題について

Posted at

下記のような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">

という型になります。

参考

3
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?