🐙

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 型をオプショナルで持たせる方法です。
実はこれ、バリデーションライブラリの zodsafeParse の戻り値で採用しているのと同じ構造です。

// 変更点:存在してはいけない側に ? で 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 での記述がスッキリする

この定義にすると、dataerr が(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?.datares?.err で直接アクセス
  • 厳格に分岐したい時: if (res.isOk) で型を絞り込む

というように、状況に応じて使い分けられるのがこのパターンの強みです。
「絶対に成功時しか処理を通したくない」というロジックでは、従来通り isOk でガードを書けば、TypeScript は完璧に型を絞り込んでくれます。

まとめ

TypeScript の never を活用することで、型の堅牢さを保ちつつ、コードの記述量を減らすことができます。

「Result 型は便利だけど、プロパティへのアクセスがいちいち面倒だな」と感じていた方は、ぜひこの Optional Never パターン を試してみてください。

Discussion