Extract<T, U>とExclude<T, U> / TypeScript一人カレンダー
こんにちは、クレスウェア株式会社の奥野賢太郎 (@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で追加されました。
先に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
のみエラーとなりました。このように、Section
のtype
を先にSectionType
をまとめておくことで、Extract<T, U>
を使って安全に指定することができます。U
にリテラル型のみを指定しても有用である場面といえば、こういったところが思いつきます。
Exclude<T, U>
Exclude<T, U>
はExtract<T, U>
の真逆です。ちょうどこの関係はPick<T, K>
, Omit<T, K>
と同じですね。もう一度ドキュメントを貼っておきましょう。
では挙動を確認します。
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