🎄

Extract<T, U>とExclude<T, U> / TypeScript一人カレンダー

2022/12/19に公開約7,200字

こんにちは、クレスウェア株式会社の奥野賢太郎 (@okunokentaro) です。本記事はTypeScript 一人 Advent Calendar 2022の8日目です。昨日は『Omit<T, K>』を紹介しました。

Extract<T, U>

昨日まで、続けてPick<T, K>, Omit<T, K>を紹介しました。これらはオブジェクト型から、指定したプロパティのみ取り出す、あるいは取り除くことができるUtility Typesです。本日紹介するExtract<T, U>, Exclude<T, U>は、それらのUnion版といったところ。TypeScript 2.8で追加されました。

https://www.typescriptlang.org/docs/handbook/utility-types.html#extracttype-union

https://www.typescriptlang.org/docs/handbook/utility-types.html#excludeuniontype-excludedmembers

先にExtract<T, U>からみていきましょう。Extract<T, U>はUnion型の中から指定したもののみを取り出します。挙動を確認しましょう。

type Union = "a" | "b" | "c" | "d";

type T11 = Extract<Union, "a">;
//   ^? "a"

type T12 = Extract<Union, "b">;
//   ^? "b"

type T13 = Extract<Union, "c" | "d">;
//   ^? "c" | "d"

type T14 = Extract<Union, "a" | "z">;
//   ^? "a"

T11, T12, T13のようにExtract<T, U>Uに指定したリテラル型によってTから該当のリテラル型が取り出されます。T14"z"のように存在しない型を指定してもエラーにはならず、この場合"a"が有効となって"a"を得ることができます。

業務でよく使う例1

このままですと最初からT11 = "a";と書けば十分では?と感じますのでもう一例いきましょう。次の例では、たびたびこのカレンダーで出てくる買い物カゴアプリ的なものを作っている想定としましょう。商品紹介ページではCMSチックな実装となり、自由に画像やリンク付き画像を掲載できる要件があるとします。

type Section =
  | { type: "image"; path: string }
  | { type: "banner"; path: string; href: string };

type T21 = Extract<Section, { type: "image" }>;
//   ^? { type: "image"; path: string }

type T22 = Extract<Section, { type: "banner" }>;
//   ^? { type: "banner"; path: string; href: string }

Extract<T, U>を業務で使うとしたら圧倒的にこちらの用途が多いです。公式サイトのドキュメントにはこのようなオブジェクトの例は載っていないため、スルーしてしまったり、Stack Overflowで偶然見つけた回答で知ったりなどもあるかもしれません。筆者も不慣れだったときに、SOの見ず知らずの先輩から教わったものでした。

なお、こういった状況では次のように宣言してもまったく間違いではありません。むしろこの書き方をする開発者のほうが多いかもしれません。こういった状況ではexportがくっついてることがとても多いですね。

export type ImageSection = {
  type: "image";
  path: string;
};

export type BannerSection = {
  type: "banner";
  path: string;
  href: string;
};

export type Section = ImageSection | BannerSection;

むやみになんでもexportしてしまうと依存関係を複雑にすることにも繋がります。状況によってはUnion型のみをexportして、Extract<T, U>で整えることで外部への露出をむやみに増やさないこともできます。

業務でよく使う例2

さて、先に紹介したtype T11 = Extract<Union, "a">;という書き方ですが、これもサンプルコードのための使い方というわけではなく、業務上でも活用できる用途があります。それはちょうど前節で紹介したtype: "image"type: "banner"の扱い。

これらを先にSectionTypeとして宣言することもできます。

type SectionType = "image" | "banner";

export type ImageSection = {
  type: "image";
  path: string;
};

export type BannerSection = {
  type: "banner";
  path: string;
  href: string;
};

export type Section = ImageSection | BannerSection;

このとき、SectionType"image"ImageSection["type"]"image"の2つのリテラル型に注目します。これらは「たまたま」同じ文字列なのか?それとも意図してSectionTypeのものを使っているのか…?ではここで、うっかりtypoしてみましょうか。

type SectionType = "image" | "banner";

export type ImageSection = {
  type: "imagee"; // ee になってしまった
  path: string;
};

typeをtypoしてしまいました。"imagee"SectionTypeには含まれないリテラル型です。しかしTypeScriptのコンパイラにはこれがtypoなのか意図して書いたのか、まったく伝わりません。なので伝わるようにしてみましょう。

type SectionType = "image" | "banner";

export type ImageSection = {
  type: Extract<SectionType, "imagee">;
  path: string;
};

const section1: ImageSection = {
  type: "image", // Error
  path: "//path/to"
};

const section2: ImageSection = {
  type: "imagee", // Error
  path: "//path/to"
};

Extract<SectionType, "imagee">のように、Extract<T, U>Uにtypoを含めると、結果はneverになります。type: neverを満たすことはできないため、変数section1, section2はいずれもErrorとなります。

ではtypoを修正しましょう。

type SectionType = "image" | "banner";

export type ImageSection = {
  type: Extract<SectionType, "image">; // OK
  path: string;
};

const section1: ImageSection = {
  type: "image", // OK
  path: "//path/to"
};

const section2: ImageSection = {
  type: "imagee", // Error
  path: "//path/to"
};

こうすることで、変数section2のみエラーとなりました。このように、Sectiontypeを先にSectionTypeをまとめておくことで、Extract<T, U>を使って安全に指定することができます。Uにリテラル型のみを指定しても有用である場面といえば、こういったところが思いつきます。

Exclude<T, U>

Exclude<T, U>Extract<T, U>の真逆です。ちょうどこの関係はPick<T, K>, Omit<T, K>と同じですね。もう一度ドキュメントを貼っておきましょう。

https://www.typescriptlang.org/docs/handbook/utility-types.html#excludeuniontype-excludedmembers

では挙動を確認します。

type Union = "a" | "b" | "c" | "d";

type T31 = Exclude<Union, "a">;
//   ^? "b" | "c" | "d"

type T32 = Exclude<Union, "b">;
//   ^? "a" | "c" | "d"

type T33 = Exclude<Union, "c" | "d">;
//   ^? "a" | "b"

type T34 = Exclude<Union, "a" | "z">;
//   ^? "b" | "c" | "d"

type T35 = Exclude<Union, never>;
//   ^? "a" | "b" | "c" | "d"

Extract<T, U>とは逆に指定した型が取り除かれています。T35のように存在しない型を指定すると、なにも取り除かれずにそのまま返る点に注意してください。

Exclude<T, U>は、Extract<T, U>に比べると業務上で直接使うことはめったにありません。ですが我々はExclude<T, U>を日頃から使っているかもしれません。

ということで昨日紹介したOmit<T, K>実装をみてみましょう

type Omit<T, K extends keyof any> = Pick<T, Exclude<keyof T, K>>;

昨日Omit<T, K>Pick<T, K>の逆だと説明しました。実装をみるとまさにその通りで、Pick<T, K>に対して「Exclude<T, U>で逆にしたもの」を渡しているのです。このように、Exclude<T, U>を単体で業務内でポンと使うことは少ないにせよ、他のなんらかと組み合わせて仕組みを作る上でこの作用は重要となります。

では最後にExtract<T, U>Exclude<T, U>の実装を並べて比較して終わりましょう。

type Exclude<T, U> = T extends U ? never : T;
type Extract<T, U> = T extends U ? T : never;

Conditional Typesの真偽がちょうど逆になっていることが確認できます。感覚的ではなく実際に論理的に逆となるような実装であることがわかりました。

明日は『Mapped Typesを活用する』

ここまでPick, Omit, Extract, Excludeの4つでオブジェクトやUnionを操作できることを紹介しました。明日はこういったUtility Typesの基礎を支えるAPIであるMapped Typesについて紹介します。それではまた。

Discussion

ログインするとコメントできます