メインコンテンツまでスキップ

判別可能なユニオン型 (discriminated union)

TypeScriptの判別可能なユニオン型は、ユニオンに属する各オブジェクトの型を区別するための「しるし」がついた特別なユニオン型です。オブジェクト型からなるユニオン型を絞り込む際に、分岐ロジックが複雑になる場合は、判別可能なユニオン型を使うとコードの可読性と保守性がよくなります。

通常のユニオン型は絞り込みが複雑になる

TypeScriptのユニオン型は自由度が高く、好きな型を組み合わせられます。次のUploadStatusはファイルアップロードの状況を表現したユニオン型です。アップロード中InProgress、アップロード成功Success、アップロード失敗Failureの組み合わせです。

ts
type UploadStatus = InProgress | Success | Failure;
type InProgress = { done: boolean; progress: number };
type Success = { done: boolean };
type Failure = { done: boolean; error: Error };
ts
type UploadStatus = InProgress | Success | Failure;
type InProgress = { done: boolean; progress: number };
type Success = { done: boolean };
type Failure = { done: boolean; error: Error };

UploadStatusの各状態を整理したのが次の表です。

意味doneprogresserror
InProgressアップロード中false進捗率(%)-
Successアップロード成功true--
Failureアップロード失敗true-エラー詳細

状態を表示する関数を実装してみます。

ts
function printStatus(status: UploadStatus) {
if (status.done === false) {
console.log(`アップロード中:${status.progress}%`);
Property 'progress' does not exist on type 'UploadStatus'. Property 'progress' does not exist on type 'Success'.2339Property 'progress' does not exist on type 'UploadStatus'. Property 'progress' does not exist on type 'Success'.
}
}
ts
function printStatus(status: UploadStatus) {
if (status.done === false) {
console.log(`アップロード中:${status.progress}%`);
Property 'progress' does not exist on type 'UploadStatus'. Property 'progress' does not exist on type 'Success'.2339Property 'progress' does not exist on type 'UploadStatus'. Property 'progress' does not exist on type 'Success'.
}
}

この実装は、donefalseであることをチェックしています。不具合はないはずです。しかし、コンパイラーにはprogressが無いと警告されます。これは、if分岐内でも、statusSuccessFailureかもしれないとコンパイラーが考えるためです。

このエラーを解消するには、progressがあることをチェックする必要があります。そうすると、コンパイラーはif分岐内のstatusInProgressだと判断します。

ts
function printStatus(status: UploadStatus) {
if (status.done === false && "progress" in status) {
// ^^^^^^^^^^^^^^^^^^^^追加
console.log(`アップロード中:${status.progress}%`);
// コンパイルエラーが解消!
}
}
ts
function printStatus(status: UploadStatus) {
if (status.done === false && "progress" in status) {
// ^^^^^^^^^^^^^^^^^^^^追加
console.log(`アップロード中:${status.progress}%`);
// コンパイルエラーが解消!
}
}

コンパイルエラーを起こさないように、すべての状態に対応した関数が次です。

ts
function printStatus(status: UploadStatus) {
if (status.done) {
if ("error" in status) {
console.log(`アップロード失敗:${status.error.message}`);
} else {
console.log("アップロード成功");
}
} else if ("progress" in status) {
console.log(`アップロード中:${status.progress}%`);
}
}
ts
function printStatus(status: UploadStatus) {
if (status.done) {
if ("error" in status) {
console.log(`アップロード失敗:${status.error.message}`);
} else {
console.log("アップロード成功");
}
} else if ("progress" in status) {
console.log(`アップロード中:${status.progress}%`);
}
}

どうでしょうか。このコードはなんだかごちゃついていませんか。あまり読みやすいとは言えないかもしれません。こうしたオブジェクトのユニオン型は、判別可能なユニオン型に書き直すとよいです。読みやすく、保守性も良くなります。

判別可能なユニオン型とは?

TypeScriptの判別可能なユニオン型(discriminated union)はユニオン型の応用です。判別可能なユニオン型は、タグ付きユニオン(tagged union)や直和型と呼ぶこともあります。

判別可能なユニオン型は次の特徴を持ったユニオン型です。

  1. オブジェクト型で構成されたユニオン型
  2. 各オブジェクト型を判別するためのプロパティ(しるし)を持つ
    • このプロパティのことをディスクリミネータ(discriminator)と呼ぶ
  3. ディスクリミネータの型はリテラル型などであること
  4. ディスクリミネータさえ有れば、各オブジェクト型は固有のプロパティを持ってもよい

たとえば、上のUploadStatusを判別可能なユニオン型に書き直すと、次のようになります。

ts
type UploadStatus = InProgress | Success | Failure;
type InProgress = { type: "InProgress"; progress: number };
type Success = { type: "Success" };
type Failure = { type: "Failure"; error: Error };
ts
type UploadStatus = InProgress | Success | Failure;
type InProgress = { type: "InProgress"; progress: number };
type Success = { type: "Success" };
type Failure = { type: "Failure"; error: Error };

これを表に整理したのが次です。

意味ディスクリミネータprogresserror
InProgressアップロード中type: "InProgress"進捗率(%)-
Successアップロード成功type: "Success"--
Failureアップロード失敗type: "Failure"-エラー詳細

変わった点といえば、done: booleanがなくなり、typeというディスクリミネータが追加されたところです。typeの型がstringではなく、InProgressなどのリテラル型になったことも重要な変更点です。

判別可能なユニオン型の絞り込み

判別可能なユニオン型は、ディスクリミネータを分岐すると型が絞り込まれます。

ts
function printStatus(status: UploadStatus) {
if (status.type === "InProgress") {
console.log(`アップロード中:${status.progress}%`);
(parameter) status: InProgress
} else if (status.type === "Success") {
console.log("アップロード成功", status);
(parameter) status: Success
} else if (status.type === "Failure") {
console.log(`アップロード失敗:${status.error.message}`);
(parameter) status: Failure
} else {
console.log("不正なステータス: ", status);
}
}
ts
function printStatus(status: UploadStatus) {
if (status.type === "InProgress") {
console.log(`アップロード中:${status.progress}%`);
(parameter) status: InProgress
} else if (status.type === "Success") {
console.log("アップロード成功", status);
(parameter) status: Success
} else if (status.type === "Failure") {
console.log(`アップロード失敗:${status.error.message}`);
(parameter) status: Failure
} else {
console.log("不正なステータス: ", status);
}
}

switch文で書いても同じく絞り込みをコンパイラーが理解します。

ts
function printStatus(status: UploadStatus) {
switch (status.type) {
case "InProgress":
console.log(`アップロード中:${status.progress}%`);
break;
case "Success":
console.log("アップロード成功", status);
break;
case "Failure":
console.log(`アップロード失敗:${status.error.message}`);
break;
default:
console.log("不正なステータス: ", status);
}
}
ts
function printStatus(status: UploadStatus) {
switch (status.type) {
case "InProgress":
console.log(`アップロード中:${status.progress}%`);
break;
case "Success":
console.log("アップロード成功", status);
break;
case "Failure":
console.log(`アップロード失敗:${status.error.message}`);
break;
default:
console.log("不正なステータス: ", status);
}
}

判別可能なユニオン型を使ったほうが、コンパイラーが型の絞り込みを理解できます。その結果、分岐処理が読みやすく、保守性も高くなります。

ディスクリミネータに使える型

ディスクリミネータに使える型は、リテラル型とnullundefinedです。

  • リテラル型
    • 文字列リテラル型: (例)"success""OK"など
    • 数値リテラル型: (例)1200など
    • 論理値リテラル型: trueまたはfalse
  • null
  • undefined

上のUploadStatusでは、文字列リテラル型をディスクリミネータに使いました。リテラル型には数値と論理値もあります。これらもディスクリミネータに使えます。

数値リテラル型のディスクリミネータ
ts
type OkOrBadRequest =
| { statusCode: 200; value: string }
| { statusCode: 400; message: string };
 
function handleResponse(x: OkOrBadRequest) {
if (x.statusCode === 200) {
console.log(x.value);
} else {
console.log(x.message);
}
}
数値リテラル型のディスクリミネータ
ts
type OkOrBadRequest =
| { statusCode: 200; value: string }
| { statusCode: 400; message: string };
 
function handleResponse(x: OkOrBadRequest) {
if (x.statusCode === 200) {
console.log(x.value);
} else {
console.log(x.message);
}
}
論理値リテラル型のディスクリミネータ
ts
type OkOrNotOk =
| { isOK: true; value: string }
| { isOK: false; error: string };
 
function handleStatus(x: OkOrNotOk) {
if (x.isOK) {
console.log(x.value);
} else {
console.log(x.error);
}
}
論理値リテラル型のディスクリミネータ
ts
type OkOrNotOk =
| { isOK: true; value: string }
| { isOK: false; error: string };
 
function handleStatus(x: OkOrNotOk) {
if (x.isOK) {
console.log(x.value);
} else {
console.log(x.error);
}
}

nullと非nullの関係にある型もディスクリミネータになれます。次の例では、errorプロパティがnullまたはErrorで、null・非nullの関係が成り立っています。

ts
type Result =
| { error: null; value: string }
| { error: Error };
 
function handleResult(result: Result) {
if (result.error === null) {
console.log(result.value);
} else {
console.log(result.error);
}
}
ts
type Result =
| { error: null; value: string }
| { error: Error };
 
function handleResult(result: Result) {
if (result.error === null) {
console.log(result.value);
} else {
console.log(result.error);
}
}

同様にundefinedもundefined・非undefinedの関係が成り立つプロパティは、ディスクリミネータになります。

ts
type Result =
| { error: undefined; value: string }
| { error: Error };
 
function handleResult(result: Result) {
if (result.error) {
console.log(result.error);
} else {
console.log(result.value);
}
}
ts
type Result =
| { error: undefined; value: string }
| { error: Error };
 
function handleResult(result: Result) {
if (result.error) {
console.log(result.error);
} else {
console.log(result.value);
}
}

ディスクリミネータを変数に代入する場合

ディスクリミネータを変数に代入し、その変数を条件分岐に使った場合も、型の絞り込みができます。

ts
type Shape =
| { type: "circle"; color: string; radius: number }
| { type: "square"; color: string; size: number };
 
function toCSS(shape: Shape) {
const { type, color } = shape;
// ^^^^ディスクリミネータ
switch (type) {
case "circle":
return {
background: color,
borderRadius: shape.radius,
(parameter) shape: { type: "circle"; color: string; radius: number; }
};
 
case "square":
return {
background: color,
width: shape.size,
height: shape.size,
(parameter) shape: { type: "square"; color: string; size: number; }
};
}
}
ts
type Shape =
| { type: "circle"; color: string; radius: number }
| { type: "square"; color: string; size: number };
 
function toCSS(shape: Shape) {
const { type, color } = shape;
// ^^^^ディスクリミネータ
switch (type) {
case "circle":
return {
background: color,
borderRadius: shape.radius,
(parameter) shape: { type: "circle"; color: string; radius: number; }
};
 
case "square":
return {
background: color,
width: shape.size,
height: shape.size,
(parameter) shape: { type: "square"; color: string; size: number; }
};
}
}
学びをシェアする

🦄TypeScriptの判別可能なユニオン型
・ディスクリミネータを持つオブジェクト型からなるユニオン型
・if/switch分岐で型が絞り込みやすい

🏷ディスクリミネータ
・各オブジェクト共通のプロパティキー(しるし的なもの)
・使える型は、リテラル型、null、undefined

『サバイバルTypeScript』より

この内容をツイートする

関連情報

📄️ ユニオン型

TypeScriptのユニオン型(union type)は「いずれかの型」を表現するものです。
  • 質問する ─ 読んでも分からなかったこと、TypeScriptで分からないこと、お気軽にGitHubまで🙂
  • 問題を報告する ─ 文章やサンプルコードなどの誤植はお知らせください。