型安全に条件分岐できてない?判別可能なユニオン型で解決!
こんにちは。ソフトウェアエンジニア(志望の26卒)の naotani です!
大学ではヨーロッパの歴史を学びつつ、課外ではwebアプリケーションの開発(主にフロントエンド)に取り組んでいます。
実は今回、初めてzennで記事を執筆しています…!
これからも情報発信活動に力を入れていこうと思っています🔥
はじめに
突然ですが、TypeScriptで条件分岐や絞り込みをするとき、
hogehogeはundefinedの可能性があります。
型hogehogeを型foobarに割り当てることはできません。
的なエラーメッセージを見たことがあると思います。
そして、そのエラーを解消しようとして型定義を修正していくと、型が冗長になったり、アクセスするときに ?
で繋がないといけないハメになっちゃうことがよくあります。
今回はそんなTypeScriptのよくある悩みを解決する判別可能なユニオン型を紹介します!
判別可能なユニオン型を使わない絞り込み
具体的なコードを例に挙げてみていきましょう。
アラビア数字とローマ数字を相互に変換するWebアプリケーションを実装します。
ローマ数字はI, M, X
のようにアルファベットなので、どちらが来てもいいように文字列で入力を受け取ります。
処理が成功したらアラビア数字やローマ数字を表す文字列を返し、失敗すればフロントでエラーメッセージを表示させるようにします。
型定義
まず、変換処理を行う関数の返り値を定義します。
result
とerrorMessage
は常に存在するわけではないのでオプショナルとしています。
type ProcessInputResult = {
status: "success" | "failure" | "waiting";
result?: string;
errorMessage?: string;
};
この型を返り値にもつ関数を定義します。
関数定義
export function processInput(input: string): ProcessInputResult {
let errorMessage: string | null = null;
// 入力値が無い/空白改行のみの状態
if (input.trim() === "") {
return {
status: "waiting",
};
}
const inputType = determineInputType(input); // アラビア・ローマ・その他を判断する
// 入力のタイプによって各々の処理を走らせる
switch (inputType) {
case "arabic":
const arabic = parseInt(input, 10);
if (arabic > 4000) {
errorMessage = "4001以上のアラビア数字は変換できません。";
return {
status: "failure",
errorMessage: errorMessage,
};
}
if (arabic <= 0) {
errorMessage = "0以下の数字は変換できません。";
return {
status: "failure",
errorMessage: errorMessage,
};
}
return {
status: "success",
result: arabicToRoman(arabic),
};
case "roman":
try {
return {
status: "success",
result: romanToArabic(input),
};
} catch (error) {
errorMessage = "無効なローマ数字です。";
return {
status: "failure",
errorMessage: errorMessage,
};
}
case "other":
errorMessage = "有効なアラビア数字またはローマ数字ではありません。";
return {
status: "failure",
errorMessage: errorMessage,
};
}
}
このとき、エラーの分岐が少々厄介なことに気づきます。
- 入力値がローマ・アラビア数字でもない場合
- 入力値がアラビア数字で1~4000以外の値の場合
- 入力値がローマ数字っぽいが不適切な場合
- 何らかのエラー
分岐がややこしいし、それぞれの結果に対してオブジェクトで返しているので若干コードが読みづらくなっています。
コンポーネント定義
コンポーネントで呼び出す箇所も確認してみましょう。
const Example = () => {
const [input, setInput] = useState("");
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
setInput(e.target.value);
};
const status = processInput(input);
return (
<div className="mx-auto flex max-w-[640px] flex-col gap-4 p-8 lg:p-16">
<Label htmlFor="convert">アラビア数字・ローマ数字を入力</Label>
<Input type="text" id="convert" value={input} onChange={handleChange} />
{status.status === "failure" && (
<Alert variant="destructive">
<AlertCircle className="h-4 w-4" />
<AlertTitle>エラー</AlertTitle>
<AlertDescription>{status.errorMessage}</AlertDescription>
</Alert>
)}
{status.status === "success" && (
<Alert variant="success">
<CircleCheck className="h-4 w-4" />
<AlertTitle>結果</AlertTitle>
<AlertDescription className="flex items-end justify-between">
<p className="text-xl">{status.result}</p>
<CopyBtn text={status.result}/>
</AlertDescription>
</Alert>
)}
<div className="">
<h3 className="mb-2 font-bold">注意</h3>
<div className="flex flex-col gap-y-1">
<p>アラビア数字は1以上9999以下の整数を入力してください。</p>
<p>ローマ数字の出入力は大文字のアルファベットで行います。</p>
</div>
</div>
</div>
);
};
processInput
関数の返り値のresultやerrorMessageはオプショナルなので、以下画像のように指摘されてしまいます。
resultがundefinedになる可能性を指摘されている
演算子??
でundefined
の場合を記述すればエラーを回避することはできますが、なんだかスマートでない気がします。
上記のような悩みを解決するのが、判別可能なユニオン型(discriminated union)です!
判別可能なユニオン型を使った絞り込み
判別可能なユニオン型とは?
判別可能なユニオン型とは、複数の型を1つの型にまとめ、それぞれの型に識別子を持たせることで、型の安全性を高める仕組みです。
具体的には、各型に共通のプロパティ(status
など)を持たせ、そのプロパティの値で型を区別します。これにより、コード内で型を絞り込むことができ、型推論を活用して安全に処理を行うことが可能です。
判別可能なユニオン型は条件分岐やパターンマッチングで有効に機能し、TypeScriptの強力な型安全性をサポートします。
型定義
type ProcessInputStatus = Success | Failure | Waiting;
type Success = {
status: "success";
result: string;
};
type Failure = {
status: "failure";
errorMessage: string;
};
type Waiting = {
status: "waiting";
};
ProcessInputStatus
は、Success
、Failure
、Waiting
からなるユニオン型です。
通常のユニオン型と違う点は、各型に共通のstatus
プロパティが存在することです。
このように、各型に共通のプロパティを用意することで、条件分岐が容易になります。
関数定義
判別可能なユニオン型を使って関数を定義します。
先ほど定義した関数と特に変わりはありません。
export function processInput(input: string): ProcessInputStatus {
if (input.trim() === "") {
return {
status: "waiting",
};
}
const inputType = determineInputType(input);
switch (inputType) {
case "arabic":
const arabic = parseInt(input, 10);
if (arabic > 4000) {
return {
status: "failure",
errorMessage: "4001以上のアラビア数字は変換できません。",
};
}
if (arabic <= 0) {
return {
status: "failure",
errorMessage: "0以下の数字は変換できません。",
};
}
return {
status: "success",
result: arabicToRoman(arabic),
};
case "roman":
try {
const arabicResult = romanToArabic(input);
return {
status: "success",
result: arabicResult,
};
} catch (error) {
return {
status: "failure",
errorMessage: "無効なローマ数字です。",
};
}
case "other":
return {
status: "failure",
errorMessage: "有効なアラビア数字またはローマ数字ではありません。",
};
}
}
コンポーネント定義
コンポーネント側も見ていきましょう。
const ConvertRomanInt = () => {
const [input, setInput] = useState("");
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
setInput(e.target.value);
};
const status = processInput(input);
return (
<div className="mx-auto flex max-w-[640px] flex-col gap-4 p-8 lg:p-16">
<Label htmlFor="convert">アラビア数字・ローマ数字を入力</Label>
<Input type="text" id="convert" value={input} onChange={handleChange} />
{status.status === "failure" && (
<Alert variant="destructive">
<AlertCircle className="h-4 w-4" />
<AlertTitle>エラー</AlertTitle>
<AlertDescription>{status.errorMessage}</AlertDescription>
</Alert>
)}
{status.status === "success" && (
<Alert variant="success">
<CircleCheck className="h-4 w-4" />
<AlertTitle>結果</AlertTitle>
<AlertDescription className="flex items-end justify-between">
<p className="text-xl">{status.result}</p>
<CopyBtn text={status.result} />
</AlertDescription>
</Alert>
)}
<div className="">
<h3 className="mb-2 font-bold">注意</h3>
<div className="flex flex-col gap-y-1">
<p>アラビア数字は1以上4000以下の整数を入力してください。</p>
<p>ローマ数字の出入力は大文字のアルファベットで行います。</p>
</div>
</div>
</div>
);
};
この実装では、下記画像のようにしっかりとstatusプロパティによる絞り込みがなされています。
resultが型安全になっている
さいごに
判別可能なユニオン型を使うことで、複雑な状態や結果を型安全かつ明確に表現できます。
これにより、コードの品質と保守性が向上し、バグの発生リスクを軽減できます。特に非同期処理や状態管理が必要なアプリケーションで有用です。
記事で使用したローマ数字/アラビア数字変換アプリケーションを紹介しておきます。
Discussion