TypeScript でコードを書いていると、「このユニオン型からこの値だけ取り除いた型がほしい」「逆に、特定の型だけ抜き出した型を作りたい」という場面がよくあります。そんなときに役立つのが、標準で用意されている Exclude<T, U> / Extract<T, U> / NonNullable<T> という3つのユーティリティ型です。この記事では、既存のユニオン型から型を絞り込んだり除外したりして新しい型を組み立てる方法を、動くコード例とともに初心者〜中級者向けに解説します。あわせて、これらがどう実装されているのかと、オブジェクト型で使うときの注意点にも触れます。
目次
既存のユニオン型を「加工」して新しい型を作りたい
ユニオン型("a" | "b" | "c" のように複数の型を | でつないだ型)を使っていると、一度定義した型を土台にして、少しだけ違う型を作りたくなることがあります。たとえば「全ステータスのうち、削除済みを除いたステータス」や「複数の型が混ざった値から null を除いたもの」などです。
こうしたときに、元の型をコピーして手で書き直すと、元の型が変わったときに追従できず、ズレの原因になります。TypeScript には、元のユニオン型から必要な型だけを取り出したり不要な型を落としたりする道具が標準で備わっているので、それを使えば元の型と連動した型を安全に作れます。まずはそれぞれの役割を整理します。
| ユーティリティ型 | やること |
|---|---|
Exclude<T, U> | ユニオン型 T から、U に代入可能な型を取り除く |
Extract<T, U> | ユニオン型 T から、U に代入可能な型だけを抜き出す(Exclude の逆) |
NonNullable<T> | T から null と undefined を取り除く |
3つとも「元の型 T を受け取り、条件に合う型だけを残した新しい型を返す」という点で共通しています。Exclude と Extract はちょうど裏表の関係で、NonNullable は null / undefined を落とすことに特化した専用版だと考えると分かりやすいです。
Exclude で不要な型を取り除く
Exclude<T, U> は、ユニオン型 T の各メンバーのうち、U に代入できるものを取り除いた型を返します。もっとも単純な例は、文字列リテラルのユニオンから特定の値を消すケースです。
// "a" | "b" | "c" から "a" を取り除く type Letters = "a" | "b" | "c"; type WithoutA = Exclude<Letters, "a">; // 結果: "b" | "c" // 複数まとめて取り除くこともできる(U 自体もユニオンでよい) type OnlyC = Exclude<Letters, "a" | "b">; // 結果: "c" const value: WithoutA = "b"; // OK // const ng: WithoutA = "a"; // エラー: 型 '"a"' を型 'WithoutA' に割り当てることはできません
第2引数の U にはユニオン型を渡せるので、"a" | "b" のように複数の型を一度に取り除けます。実務では、たとえば注文ステータスの型から「キャンセル済み」を除いた「有効なステータス」を作る、といった使い方が便利です。
// 注文が取りうる全ステータス
type OrderStatus = "pending" | "paid" | "shipped" | "canceled";
// キャンセル以外を「有効なステータス」として扱いたい
type ActiveStatus = Exclude<OrderStatus, "canceled">;
// 結果: "pending" | "paid" | "shipped"
// 発送処理は有効なステータスだけを受け取る
function ship(status: ActiveStatus): void {
console.log(`${status} の注文を発送します`);
}
ship("paid"); // OK
// ship("canceled"); // エラー: "canceled" は ActiveStatus に含まれない
元の OrderStatus にステータスを追加すると、ActiveStatus にも自動的に反映されます(除外している "canceled" 以外はすべて残るため)。元の型と連動する型を作れるのが、手書きでユニオンを書き直すより優れている点です。
Extract で必要な型だけを抜き出す
Extract<T, U> は Exclude の逆で、T のメンバーのうち U に代入できるものだけを残します。「除外」ではなく「抽出」なので、欲しい型を明示して取り出したいときに使います。
type Mixed = string | number | boolean; // string に代入できる型だけを抜き出す type OnlyString = Extract<Mixed, string>; // 結果: string // 文字列リテラルのユニオンから一部だけ抜き出す type Direction = "top" | "right" | "bottom" | "left"; type Horizontal = Extract<Direction, "right" | "left">; // 結果: "right" | "left"
判別可能なユニオン(それぞれのオブジェクトが kind のような共通の目印を持つユニオン型)から、特定の種類のオブジェクト型だけを取り出したいときにも Extract は役立ちます。次の例では、図形を表すユニオンから kind が "circle" の型だけを抜き出しています。
type Shape =
| { kind: "circle"; radius: number }
| { kind: "square"; size: number }
| { kind: "rectangle"; width: number; height: number };
// kind が "circle" のメンバーだけを抜き出す
type Circle = Extract<Shape, { kind: "circle" }>;
// 結果: { kind: "circle"; radius: number }
function getArea(circle: Circle): number {
return circle.radius * circle.radius * Math.PI;
}
getArea({ kind: "circle", radius: 10 }); // OK
{ kind: "circle" } という「目印だけを持つ型」を U に渡すと、その形に代入できるメンバー(=kind が "circle" のもの)だけが残ります。ユニオンの各要素を個別の型名として取り出せるので、その型を引数や戻り値に使い回せて便利です。
NonNullable で null と undefined を落とす
NonNullable<T> は、T から null と undefined を取り除いた型を返します。API のレスポンスやオプショナルな値のように「値があるかもしれないし、ないかもしれない」型を、値が確実にある状態として扱いたいときに使います。
type MaybeName = string | null | undefined;
// null と undefined を取り除く
type Name = NonNullable<MaybeName>;
// 結果: string
// string | number | null から null を落として安全に扱う
type Id = string | number | null;
type SafeId = NonNullable<Id>;
// 結果: string | number
function printId(id: SafeId): void {
// ここでは id が null になり得ないので、そのまま使える
console.log(id.toString());
}
printId(123); // OK
printId("abc"); // OK
// printId(null); // エラー: null は SafeId に含まれない
実は NonNullable<T> は Exclude<T, null | undefined> と同じ意味です。つまり「null と undefined を除外する」という頻出パターンに、分かりやすい名前を付けた専用版だと言えます。自分で Exclude<T, null | undefined> と書いても同じ結果になりますが、意図が伝わりやすい NonNullable を使うのが読みやすくおすすめです。
これらの型はどう作られているのか
Exclude や Extract は魔法ではなく、条件型(Conditional Types)と、ユニオン型に対する分配(distributive)という仕組みで実装されています。標準ライブラリでの定義は次のようになっています。
// 標準ライブラリでの定義(イメージ)
type Exclude<T, U> = T extends U ? never : T;
type Extract<T, U> = T extends U ? T : never;
type NonNullable<T> = T & {};
ポイントは、条件型の判定対象 T がユニオン型のときは、各メンバーごとに分配して判定される点です。Exclude<"a" | "b", "a"> であれば "a" と "b" がそれぞれ extends "a" で判定され、条件に合った "a" は never に、合わなかった "b" はそのまま残り、最終的に "b" だけのユニオンになります。never はユニオンの中では消える(何もない型として吸収される)ため、結果的に「取り除く」動作になるわけです。仕組みを深く覚える必要はありませんが、「条件型で1つずつ振り分けている」とイメージできると、次に説明するつまずきが理解しやすくなります。
オブジェクト型のユニオンで意図とずれるとき
Exclude は「U に代入可能な型を消す」という判定をします。文字列リテラルなら「同じ値かどうか」で直感どおりに動きますが、オブジェクト型のユニオンでは、TypeScript の構造的部分型(プロパティの形が合えば代入可能とみなす仕組み)によって、意図しない型まで消えてしまうことがあります。
type A = { id: number };
type B = { id: number; name: string };
type Union = A | B;
// 「A を取り除いて B だけ残したい」つもりで書くと…
type WantOnlyB = Exclude<Union, A>;
// 結果: never (B まで消えてしまう)
B({ id: number; name: string })は A({ id: number })に代入可能です。B は id を持っているので、A が求める形を満たしているからです。そのため B extends A が真になり、Exclude は B まで「A に代入できる型」とみなして取り除いてしまいます。結果は A も B も消えて never になります。
こうしたオブジェクト型のユニオンを絞り込みたいときは、Exclude で構造ごと比較するのではなく、判別可能なユニオンの目印(kind や type のようなリテラルのプロパティ)を用意し、その目印で Extract / Exclude するのが安全です。次のように書けば、構造ではなくリテラルの一致で判定されるため、意図どおりに絞り込めます。
type A = { kind: "a"; id: number };
type B = { kind: "b"; id: number; name: string };
type Union = A | B;
// 目印(kind)で判定すれば、狙った型だけを残せる
type OnlyB = Exclude<Union, { kind: "a" }>;
// 結果: B({ kind: "b"; id: number; name: string })
type PickedA = Extract<Union, { kind: "a" }>;
// 結果: A({ kind: "a"; id: number })
kind という文字列リテラルのプロパティで比較すれば、"a" と "b" は互いに代入できないため、構造的部分型による巻き込みが起きません。オブジェクト型のユニオンを扱うときは、こうした目印を持たせておくと Exclude / Extract が安定して使えます。
まとめ
Exclude<T, U> はユニオン型 T から U に代入可能な型を取り除き、Extract<T, U> はその逆で U に代入可能な型だけを抜き出します。NonNullable<T> は null と undefined を取り除く専用版で、Exclude<T, null | undefined> と同じ意味です。いずれも既存のユニオン型を土台に、連動した新しい型を安全に組み立てられるのが利点です。これらは条件型と分配の仕組みで実装されているため、オブジェクト型のユニオンでは構造的部分型によって意図しない型まで消えることがあります。オブジェクトを絞り込むときは kind のような目印を用意し、そのリテラルで判定するのが安全です。文字列リテラルのユニオンには直感どおりに、オブジェクトのユニオンには目印を添えて、と使い分けると型の加工がぐっと楽になります。