Typescript の Result 型を自作するときは optional never を使う事をおすすめしたい
はじめに
TypeScript でエラーハンドリングをするとき、throw を使うと型情報が消えてしまったり、try-catch のネストが深くなったりして困ることがありますよね。
そんなとき、neverthrow のようなライブラリを入れるほどではないけれど、もっと扱いやすい形にしたい……という理由で Result 型 を自作するケースは多いと思います。
この記事では、よくある Result 型を少し改良して、劇的に使い勝手を良くする「Optional Never」パターンを紹介します。
よくある Result 型とその悩み
多くの記事で紹介されているのは、以下のような構造です。
type Ok<T> = { isOk: true; data: T };
type Err<E> = { isOk: false; err: E };
type Result<T, E> = Ok<T> | Err<E>;
こうしておけば、isOk フラグで分岐できて安全です。
const res = await resultFunction();
if (!res.isOk) {
// ここで Err 型に確定
console.error(res.err);
return;
}
// ここで Ok 型に確定
console.log(res.data);
サーバーサイドのロジックなどではこれで十分なのですが、フロントエンド(特に React)や、API クライアントとして使う場合に**「ちょっと面倒だな」**と感じる瞬間があります。
悩み1:プロパティへのアクセスが面倒
例えば、React で「エラーがあったら表示する」という単純な処理を書くとき。
// res.err に触るには isOk のチェックが必須
{!res.isOk && <p>{res.err.message}</p>}
さらに、まだデータがない状態(Result | undefined)も含めると、記述はもっと長くなります。
// undefined チェック + isOk チェック
{res?.isOk === false && <p>{res.err.message}</p>}
「エラーがあるなら出してくれればいいのに」と思うところですが、型ガードのために条件式が長くなりがちです。
悩み2:実は型に「抜け穴」がある
TypeScript は構造的サブタイピング(形の合うものは許容する)なので、実はこんなオブジェクトも Result 型として通ってしまいます。
// isOk: true なのに err も持っている変なオブジェクト
const lied = { isOk: true as const, data: "ok", err: "error!!" };
// コンパイルエラーにならない!
const func = (res: Result<string, string>) => res;
func(lied);
Result 型を作る関数を自作していれば防げますが、型定義として少し隙がある状態です。
提案:Optional Never パターン
そこで提案したいのが、never 型をオプショナルで持たせる方法です。
実はこれ、バリデーションライブラリの zod が safeParse の戻り値で採用しているのと同じ構造です。
// 変更点:存在してはいけない側に ? で never を持たせる
type Ok<T> = { isOk: true; data: T; err?: never };
type Err<E> = { isOk: false; err: E; data?: never };
type Result<T, E> = Ok<T> | Err<E>;
一見不思議な書き方ですが、これが開発体験(DX)を大きく向上させます。
メリット1:React での記述がスッキリする
この定義にすると、data や err が(undefined かもしれないけれど)常に存在することになります。そのため、オプショナルチェイニング (?.) が使えます。
// isOk を確認しなくても、「err があれば表示」と書ける
{res?.err && <p>{res.err.message}</p>}
res.err が存在するということは、型定義上 isOk は必ず false だと推論されるため、型安全性も保たれたままです。
メリット2:変なオブジェクトを作らせない
先ほどの「Ok なのに err がある」ケースも、型レベルで弾けるようになります。
const lied = { isOk: true as const, data: "ok", err: "error!!" };
// コンパイルエラー!
// 型 'string' を型 'undefined' に割り当てることはできません
const r: Result<string, string> = lied;
err?: never は「もし err プロパティがあるなら、値は undefined でなければならない」という制約になるため、意図しないデータの混入を防げます。
補足:isOk はもう不要?
ここまで読むと「じゃあ isOk はもう要らないの?」と思われるかもしれませんが、そんなことはありません。
-
サクッと使いたい時:
res?.dataやres?.errで直接アクセス -
厳格に分岐したい時:
if (res.isOk)で型を絞り込む
というように、状況に応じて使い分けられるのがこのパターンの強みです。
「絶対に成功時しか処理を通したくない」というロジックでは、従来通り isOk でガードを書けば、TypeScript は完璧に型を絞り込んでくれます。
まとめ
TypeScript の never を活用することで、型の堅牢さを保ちつつ、コードの記述量を減らすことができます。
「Result 型は便利だけど、プロパティへのアクセスがいちいち面倒だな」と感じていた方は、ぜひこの Optional Never パターン を試してみてください。
Discussion