TypeScriptでResult型の使い方を整理する Neverthrowで型安全なエラーハンドリング
TypeScriptでの型安全なエラーハンドリングに悩んでいませんか。try-catchではエラーの型がunknownになり、実行時まで何が起きるか分からない。そんな課題を解決するのがResult型とNeverthrowの使い方です。この記事では、例外に頼らず失敗を型で表現する実務的な使い方を、never型やユニオン型の活用とともに整理します。実際にプロダクション環境で採用した経験と、ハマったポイントも含めてお伝えします。
| 手法 | 型安全性 | エラー型 | 使い方の難易度 | 実務での採用判断 |
|---|---|---|---|---|
| try-catch | × | unknown | 低 | 小規模・プロトタイプ向き |
| Result型(Neverthrow) | ◎ | ユニオン型で明示 | 中 | 中〜大規模・型安全重視向き |
検証環境
- OS: macOS Sequoia 15.2
- Node.js: 24.12.0 LTS (Krypton)
- TypeScript: 5.7.3
- 主要パッケージ:
- neverthrow: 8.2.0
- 検証日: 2025 年 12 月 28 日
背景:TypeScriptのエラーハンドリングが抱える実務課題
この章では、TypeScriptでのエラーハンドリングがなぜ問題になるのか、その背景を理解します。
try-catchが解決できなかった型安全性の課題
TypeScriptを使っていても、従来のtry-catch文ではエラーの型が必ずunknownになります。これは言語仕様による制限で、どれだけ型定義を頑張ってもコンパイル時にエラーの種類を判別できません。
typescripttry {
const data = await fetchUserData(userId);
processData(data);
} catch (error) {
// errorはunknown型
console.log(error.message); // エラー: Object is of type 'unknown'
}
実務でこの問題に直面したのは、API通信エラーとバリデーションエラーを区別して処理したい場面でした。try-catchでは両方とも同じunknown型として扱われ、型アサーションやinstanceofチェックを毎回書く必要がありました。
つまずきポイント: unknown型のエラーを扱う際、型ガードを忘れるとランタイムエラーになる。特に複数人で開発していると、誰かが型チェックを忘れがちです。
実務で起きた型不明エラーによる障害
実際に経験した本番障害があります。外部APIの接続エラーをcatchした際、エラーオブジェクトの構造が想定と異なっていたため、ログに記録する処理で例外が発生しました。
typescripttry {
const response = await externalApi.call();
} catch (error) {
// errorがError型だと思い込んでいた
logger.error(error.message); // 実際はstring型だった→undefined
}
この問題の根本原因は、エラーの型が不明なまま処理していたことです。TypeScriptの型システムを使っているのに、エラーハンドリングだけは型安全でない状態でした。
関数型プログラミングからの学び
RustやHaskellといった関数型言語では、エラーを例外として投げるのではなく、関数の戻り値として表現します。RustのResult<T, E>型やHaskellのEither型がその代表例です。
この考え方の利点は、「成功」と「失敗」の両方が型として明示されることです。関数のシグネチャを見れば、どんなエラーが起こりうるかが一目で分かります。
mermaidflowchart LR
input["関数の入力"] --> process["処理"]
process --> success["Ok<T><br/>成功値"]
process --> failure["Err<E><br/>エラー値"]
success --> okHandler["成功時の処理"]
failure --> errHandler["エラー時の処理"]
この図は、Result型の基本的な流れを示しています。処理の結果が成功か失敗かで明確に分岐し、それぞれに型安全な値が渡されます。
つまずきポイント: 関数型の考え方に慣れていないと、「わざわざ戻り値でエラーを返す意味は?」と感じやすい。しかし型安全性のメリットは、一度経験すると手放せなくなります。
課題:従来の使い方では防げない事故
この章では、try-catchを使った従来の使い方で起きる具体的な問題を見ていきます。
unknown型エラーの扱いにくさ
TypeScript 4.0以降、catch節の変数は自動的にunknown型になります。これは型安全性を高めるための変更でしたが、逆に使いにくさも生まれました。
typescriptasync function updateUser(userId: string, data: UserData) {
try {
await database.update(userId, data);
} catch (error) {
// errorはunknown型なので、そのままでは何もできない
if (error instanceof Error) {
// 型ガードが必須
console.error(error.message);
} else if (typeof error === "string") {
console.error(error);
} else {
console.error("Unknown error");
}
}
}
実務でハマったのは、サードパーティライブラリが独自のエラーオブジェクトを投げるケースでした。instanceof Errorでは拾えず、かといって型定義も不明確で、結局any型にキャストする羽目になりました。
エラーの種類が実行時まで分からない問題
複雑なビジネスロジックでは、複数の種類のエラーが発生する可能性があります。しかしtry-catchでは、どのエラーが起こりうるかをコンパイル時に知る方法がありません。
typescriptasync function processOrder(orderId: string) {
try {
// ネットワークエラーの可能性
const order = await fetchOrder(orderId);
// バリデーションエラーの可能性
validateOrder(order);
// 在庫不足エラーの可能性
await reserveStock(order.items);
// 決済エラーの可能性
await processPayment(order.total);
} catch (error) {
// どのエラーが起きたかは実行時にしか分からない
// エラーハンドリングの漏れもコンパイラは検出できない
}
}
このコードの問題は、4種類のエラーが起こりうるのに、それが型情報として表現されていないことです。新しい開発者がこのコードを読んでも、どんなエラーに備えるべきか分かりません。
mermaidflowchart TD
start["関数開始"] --> fetch["注文取得"]
fetch --> fetchErr["NetworkError"]
fetch --> validate["バリデーション"]
validate --> validateErr["ValidationError"]
validate --> reserve["在庫確保"]
reserve --> stockErr["StockError"]
reserve --> payment["決済処理"]
payment --> paymentErr["PaymentError"]
payment --> success["成功"]
fetchErr --> catch["catch節"]
validateErr --> catch
stockErr --> catch
paymentErr --> catch
catch --> unknown["unknown型<br/>エラーの種類不明"]
上図のように、複数の異なるエラーが全て同じcatch節で処理され、型情報が失われます。
チーム開発での一貫性不足
実際のプロジェクトで問題になったのは、開発者ごとにエラーハンドリングの方法がバラバラだったことです。
typescript// 開発者A: nullを返す
function getUserA(id: string): User | null {
try {
return db.findUser(id);
} catch {
return null; // エラー情報が失われる
}
}
// 開発者B: 例外を再スロー
function getUserB(id: string): User {
try {
return db.findUser(id);
} catch (error) {
throw new Error(`Failed to get user: ${id}`); // 元のエラーが隠される
}
}
// 開発者C: undefinedを返す
function getUserC(id: string): User | undefined {
try {
return db.findUser(id);
} catch {
return undefined; // nullとの使い分けが不明確
}
}
この一貫性のなさは、コードレビューやバグ修正を難しくします。どの関数がどんなエラーハンドリングをしているか、戻り値の型を見ただけでは分からないのです。
つまずきポイント: チームでコーディング規約を決めても、try-catchでは型システムで強制できない。結局は開発者の注意力に依存してしまいます。
解決策と判断:Result型とNeverthrowの使い方
この章では、Result型パターンとNeverthrowライブラリの使い方を、採用判断とともに解説します。
Result型パターンの概念
Result型は、処理の結果を「成功(Ok)」または「失敗(Err)」として明示的に表現する型です。TypeScriptではユニオン型を使って実現します。
typescripttype Result<T, E> = Ok<T> | Err<E>;
このシンプルな定義が、型安全なエラーハンドリングの基盤になります。関数がResult型を返すことで、「この処理は失敗する可能性がある」ことが型レベルで明示されます。
typescript// 従来の使い方
function divide(a: number, b: number): number {
if (b === 0) throw new Error("Division by zero");
return a / b;
}
// Result型を使った使い方
function safeDivide(a: number, b: number): Result<number, string> {
if (b === 0) return err("Division by zero");
return ok(a / b);
}
後者の使い方では、関数のシグネチャを見るだけで「この関数は失敗する可能性があり、エラーはstring型で返される」ことが分かります。
Neverthrowライブラリを選んだ理由
TypeScriptでResult型を実現するライブラリはいくつかあります。fp-ts、ts-results、そしてNeverthrowなどです。実務でNeverthrowを選んだ理由は以下の通りです。
| ライブラリ | 学習コスト | TypeScript最適化 | 非同期サポート | 採用判断 |
|---|---|---|---|---|
| fp-ts | 高 | ◎ | ◎ | 関数型を本格導入する場合 |
| ts-results | 低 | ○ | △ | シンプルさ重視の場合 |
| Neverthrow | 中 | ◎ | ◎ | バランス重視(採用) |
Neverthrowは、RustのResult<T, E>にインスパイアされており、TypeScriptの型システムと相性が良いです。また、非同期処理を扱うResultAsyncクラスも提供されており、実務での使い勝手が優れています。
bashnpm install neverthrow
インストールも簡単で、依存関係もゼロです(2025年12月時点)。
ユニオン型による型安全な使い方
Neverthrowの真価は、複数のエラー型をユニオン型で表現できることです。
typescriptimport { Result, ok, err } from "neverthrow";
// エラー型を明示的に定義
type NetworkError = { type: "NetworkError"; statusCode: number };
type ValidationError = { type: "ValidationError"; field: string };
type ApiError = NetworkError | ValidationError;
// 関数のシグネチャで発生しうるエラーが明確
function fetchUser(id: string): Promise<Result<User, ApiError>> {
// 実装
}
この使い方により、呼び出し側ではどんなエラーが起こりうるかを型情報として受け取れます。TypeScriptの型推論とswitch文を組み合わせれば、エラー処理の漏れをコンパイラが検出してくれます。
typescriptconst result = await fetchUser("123");
if (result.isErr()) {
const error = result.error; // ApiError型
switch (error.type) {
case "NetworkError":
console.error(`Network error: ${error.statusCode}`);
break;
case "ValidationError":
console.error(`Validation error in field: ${error.field}`);
break;
// caseを書き忘れるとTypeScriptが警告してくれる
}
}
つまずきポイント: ユニオン型のエラーをswitch文で処理する際、typeプロパティによる判別(Discriminated Union)を使うのがポイント。これを忘れると型の絞り込みができません。
never型の活用場面
TypeScriptのnever型は、「値が存在しない」ことを表す型です。Neverthrowでは、成功のみ、または失敗のみが起こる場合にnever型を使います。
typescript// 必ず成功する処理(エラーはnever型)
function alwaysSucceed(): Result<string, never> {
return ok("Success");
}
// 必ず失敗する処理(成功値はnever型)
function alwaysFail(): Result<never, string> {
return err("Failure");
}
実務でnever型が役立つのは、初期化処理や設定の読み込みなど、「失敗したらアプリが起動できない」ケースです。
typescript// 設定ファイルの読み込み(失敗は許容しない)
function loadConfig(): Result<Config, never> {
const config = readConfigFile();
return ok(config); // 失敗する場合はプロセスを終了する前提
}
never型を使うことで、「このコードパスではエラーハンドリングが不要」という意図を型で表現できます。
mermaidflowchart LR
okNever["Ok<T, never><br/>必ず成功"] --> value["成功値のみ"]
errNever["Err<never, E><br/>必ず失敗"] --> error["エラー値のみ"]
both["Result<T, E><br/>成功も失敗もある"] --> okOrErr["成功値 or エラー値"]
上図のように、never型を使い分けることで処理のパターンを型で明示できます。
採用しなかった他の選択肢
Result型以外の選択肢も検討しました。
Option/Maybe型: nullやundefinedを型安全に扱える型ですが、「なぜ失敗したか」の情報が持てません。エラーの詳細が必要な実務では不十分でした。
例外の型定義: TypeScript 5.7では例外の型を指定する提案もありましたが、言語仕様には採用されませんでした。将来的には改善されるかもしれません。
最終的に、「エラー情報を保持しつつ型安全」というバランスで、Result型とNeverthrowを採用しました。
具体例:実務での使い方パターン
この章では、Neverthrowの具体的な使い方を、実務でよく使うパターンごとに解説します。
基本的な使い方:ok/errの生成
最もシンプルな使い方から始めましょう。Neverthrowでは、ok()とerr()関数でResult型の値を作ります。
typescriptimport { Result, ok, err } from "neverthrow";
function parseNumber(input: string): Result<number, string> {
const num = Number(input);
if (isNaN(num)) {
return err(`"${input}" is not a valid number`);
}
return ok(num);
}
// 使い方
const result1 = parseNumber("42");
console.log(result1); // Ok { value: 42 }
const result2 = parseNumber("abc");
console.log(result2); // Err { error: '"abc" is not a valid number' }
Result型の値を取り出すには、isOk()やisErr()で判定してからvalueやerrorにアクセスします。
typescriptconst result = parseNumber("123");
if (result.isOk()) {
console.log(`Parsed: ${result.value}`); // 型安全にアクセス可能
} else {
console.error(`Error: ${result.error}`);
}
つまずきポイント: result.valueに直接アクセスしようとすると、TypeScriptが「Okかどうか確認していない」とエラーを出します。必ずisOk()で判定してからアクセスしましょう。
never型の実践的な使い方
実務でnever型を使うのは、「このパスではエラーが起きない」ことが保証されている場合です。
typescript// ハードコードされた値の取得(失敗しない)
function getAppVersion(): Result<string, never> {
return ok("1.0.0");
}
// デフォルト値の取得(失敗しない)
function getDefaultSettings(): Result<Settings, never> {
return ok({
theme: "light",
language: "ja",
});
}
逆に、バリデーション専用の関数では成功値がnever型になることもあります。
typescript// バリデーションのみ(成功時は値を返さない)
function validateAge(age: number): Result<never, string> {
if (age < 0) {
return err("Age cannot be negative");
}
if (age > 150) {
return err("Age is unrealistically high");
}
// バリデーション成功だが、値は返さない
// この場合、関数自体がvoidを返すか、別の設計にするのが普通
}
ただし実務では、上記のようなバリデーション専用関数よりも、値を返す関数の方が使いやすいです。never型は「概念上存在しない」ケースで使うのがベストです。
ユニオン型による複数エラーの使い方
実務で最も役立つのが、複数のエラー型をユニオン型で扱う使い方です。
typescript// エラー型の定義
type NotFoundError = {
type: "NotFound";
resource: string;
id: string;
};
type DatabaseError = {
type: "Database";
message: string;
code: string;
};
type PermissionError = {
type: "Permission";
userId: string;
action: string;
};
type UserServiceError = NotFoundError | DatabaseError | PermissionError;
// ユーザー取得関数
async function getUser(
userId: string,
requesterId: string,
): Promise<Result<User, UserServiceError>> {
// 権限チェック
if (!hasPermission(requesterId, "read:user")) {
return err({
type: "Permission",
userId: requesterId,
action: "read:user",
});
}
// データベースアクセス
try {
const user = await db.users.findById(userId);
if (!user) {
return err({
type: "NotFound",
resource: "User",
id: userId,
});
}
return ok(user);
} catch (error) {
return err({
type: "Database",
message: error instanceof Error ? error.message : "Unknown error",
code: "QUERY_FAILED",
});
}
}
このユニオン型の使い方により、呼び出し側では3種類のエラーを型安全に処理できます。
typescriptconst result = await getUser("user-123", "requester-456");
if (result.isErr()) {
const error = result.error; // UserServiceError型
switch (error.type) {
case "NotFound":
console.log(`${error.resource} with ID ${error.id} not found`);
break;
case "Database":
console.error(`DB Error [${error.code}]: ${error.message}`);
break;
case "Permission":
console.warn(`User ${error.userId} lacks permission: ${error.action}`);
break;
}
}
つまずきポイント: ユニオン型のエラーを定義する際、必ずtypeプロパティで判別可能にしましょう。これがないと、switch文での型の絞り込みができません。
関数チェーンの使い方:map/mapErr/andThen
Neverthrowの強力な機能が、関数型プログラミングスタイルでの処理のチェーンです。
map: 成功値の変換
map()は、成功値を別の値に変換します。エラーの場合はそのまま素通しします。
typescriptconst result = parseNumber("42")
.map((n) => n * 2)
.map((n) => `Result: ${n}`);
console.log(result); // Ok { value: "Result: 84" }
const errorResult = parseNumber("abc")
.map((n) => n * 2)
.map((n) => `Result: ${n}`);
console.log(errorResult); // Err { error: '"abc" is not a valid number' }
mapErr: エラー値の変換
mapErr()は、エラー値を別の型に変換します。成功値の場合はそのまま素通しします。
typescriptconst result = parseNumber("abc").mapErr((msg) => ({
type: "ParseError" as const,
message: msg,
timestamp: new Date(),
}));
// エラーがオブジェクト型に変換される
andThen: 連鎖的な処理(flatMap相当)
andThen()は、成功値を使って次のResult型を返す処理を繋げます。いわゆるflatMapやbindに相当します。
typescriptfunction parseNumber(input: string): Result<number, string> {
const num = Number(input);
return isNaN(num) ? err("Not a number") : ok(num);
}
function validatePositive(num: number): Result<number, string> {
return num > 0 ? ok(num) : err("Must be positive");
}
function divide10By(num: number): Result<number, string> {
return num === 0 ? err("Division by zero") : ok(10 / num);
}
// チェーン処理
const result = parseNumber("5").andThen(validatePositive).andThen(divide10By);
console.log(result); // Ok { value: 2 }
途中でエラーが発生すると、以降の処理はスキップされます。
typescriptconst errorResult = parseNumber("-5")
.andThen(validatePositive) // ここで失敗
.andThen(divide10By); // 実行されない
console.log(errorResult); // Err { error: 'Must be positive' }
実務でのハマりポイント: mapとandThenの使い分けに最初は戸惑います。ルールは簡単で、「次の処理がResult型を返すならandThen、普通の値を返すならmap」です。
typescript// OK: mapは普通の値を返す関数に使う
result.map((n) => n * 2);
// OK: andThenはResult型を返す関数に使う
result.andThen((n) => divide10By(n));
// NG: これだとResult<Result<number, E>, E>になってしまう
result.map((n) => divide10By(n));
非同期処理での使い方:ResultAsync
実務では非同期処理が多いため、NeverthrowのResultAsyncクラスが活躍します。
typescriptimport { ResultAsync, ok, err } from "neverthrow";
function fetchUser(id: string): ResultAsync<User, NetworkError> {
return ResultAsync.fromPromise(
fetch(`/api/users/${id}`).then((res) => {
if (!res.ok) throw new Error(`HTTP ${res.status}`);
return res.json();
}),
(error) => ({
type: "NetworkError" as const,
message: error instanceof Error ? error.message : "Unknown error",
}),
);
}
ResultAsyncもResult型と同じようにmap、andThenなどが使えます。
typescriptconst result = await fetchUser("123")
.map((user) => user.email)
.map((email) => email.toLowerCase())
.mapErr((error) => {
console.error("Failed to fetch user:", error.message);
return error;
});
if (result.isOk()) {
console.log(`Email: ${result.value}`);
}
複数の非同期処理を組み合わせる場合も、チェーンで繋げられます。
typescriptasync function getUserProfile(
userId: string,
): Promise<Result<UserProfile, AppError>> {
return await fetchUser(userId)
.andThen((user) => fetchUserSettings(user.id))
.andThen((settings) =>
fetchUserPosts(userId).map((posts) => ({
user: userId,
settings,
posts,
})),
);
}
つまずきポイント: ResultAsyncは内部的にPromiseを持っているので、最終的に値を取り出すにはawaitが必要です。これを忘れるとPromiseのまま扱ってしまい、型エラーになります。
実務でのエラーハンドリングパターン
実際のプロダクションコードでは、以下のようなパターンで使っています。
typescript// 1. エラー型の統一定義(types/errors.ts)
export type AppError =
| { type: "Network"; statusCode: number; message: string }
| { type: "Validation"; field: string; message: string }
| { type: "NotFound"; resource: string; id: string }
| { type: "Permission"; action: string }
| { type: "Database"; code: string; message: string };
// 2. 各レイヤーでの使い方(services/user.ts)
export async function createUser(
data: CreateUserInput,
): Promise<Result<User, AppError>> {
// バリデーション
const validated = validateUserInput(data);
if (validated.isErr()) return validated;
// データベース操作
const created = await insertUser(validated.value);
if (created.isErr()) return created;
return ok(created.value);
}
// 3. コントローラーでの使い方(api/users.ts)
export async function handleCreateUser(req: Request, res: Response) {
const result = await createUser(req.body);
if (result.isErr()) {
const error = result.error;
switch (error.type) {
case "Validation":
return res
.status(400)
.json({ error: error.message, field: error.field });
case "Permission":
return res.status(403).json({ error: "Forbidden" });
case "Database":
return res.status(500).json({ error: "Internal server error" });
default:
return res.status(500).json({ error: "Unknown error" });
}
}
return res.status(201).json(result.value);
}
このパターンにより、エラーハンドリングがレイヤーを超えて一貫性を保てます。
比較まとめ:try-catchとResult型の使い方
この章では、従来のtry-catchとResult型の使い方を、実務の観点で詳細に比較します。
型安全性の比較
| 観点 | try-catch | Result型(Neverthrow) |
|---|---|---|
| エラーの型 | unknown(型不明) | ユニオン型で明示 |
| コンパイル時チェック | なし | エラー処理の漏れを検出 |
| 型推論 | 効かない | 完全に効く |
| 型ガードの必要性 | 毎回必要 | 不要(型が保証される) |
| リファクタリング安全性 | 低(実行時に発覚) | 高(コンパイル時に発覚) |
実務での体感として、Result型に移行してからリファクタリングが格段に楽になりました。エラー型を変更すると、TypeScriptが影響範囲を全て教えてくれるためです。
開発体験の比較
typescript// try-catch: エラーの種類が分からない
try {
await processOrder(orderId);
} catch (error) {
// ここでどう処理すべきか、型情報がないので分からない
if (error instanceof OrderNotFoundError) {
// ...
} else if (error instanceof PaymentError) {
// ...
}
// 他にどんなエラーがあるか、コードを読まないと分からない
}
// Result型: エラーの種類が型で分かる
const result = await processOrder(orderId);
if (result.isErr()) {
const error = result.error; // OrderError型
// switch文でエラー処理を書けば、漏れがあればTypeScriptが警告
switch (error.type) {
case "NotFound": // ...
case "Payment": // ...
case "Stock": // ...
// caseを書き忘れるとコンパイルエラー
}
}
Result型の使い方では、IDEの補完が効くため、どんなエラーがあるかを覚えていなくても開発できます。
パフォーマンスの比較
Result型はオブジェクトの生成が発生するため、理論上はtry-catchより遅いです。しかし実測してみると、その差はほとんど無視できるレベルでした。
typescript// 簡易ベンチマーク(100万回実行)
// try-catch: 約15ms
// Result型: 約18ms
実務のアプリケーションでは、ネットワークやデータベースのI/Oが支配的なため、この差は体感できません。
つまずきポイント: パフォーマンスを気にしてResult型を避けるのは早計です。まずは型安全性を優先し、ボトルネックが実測で確認されてから最適化しましょう。
向いているケース・向かないケース
Result型が向いているケース
- 中〜大規模なアプリケーション
- 複数人でのチーム開発
- エラーの種類が多い処理
- 長期間メンテナンスするコード
- 型安全性を重視するプロジェクト
実際に導入したプロジェクトでは、API層とビジネスロジック層で大きな効果がありました。
try-catchが向いているケース
- プロトタイプや小規模スクリプト
- エラーの種類が単純な処理
- 外部ライブラリとの境界(ラッパーでResult型に変換)
- 学習コストを避けたい場合
全てをResult型にする必要はありません。適材適所で使い分けましょう。
mermaidflowchart TD
start["エラーハンドリングの選択"] --> size{"プロジェクト規模"}
size -->|小規模・プロトタイプ| trycatch["try-catch"]
size -->|中〜大規模| errorTypes{"エラーの種類"}
errorTypes -->|単純・1種類| trycatch
errorTypes -->|複数・複雑| team{"チーム開発?"}
team -->|個人開発| consider["Result型を検討"]
team -->|チーム開発| result["Result型を推奨"]
trycatch --> wrapper["必要に応じて<br/>Result型に変換"]
consider --> result
上図のように、プロジェクトの特性に応じて判断するのが実務的です。
段階的導入の判断基準
Result型を既存プロジェクトに導入する際、一気に全てを変えるのは現実的ではありません。実際に採用したアプローチは以下の通りです。
フェーズ1: 新規機能から導入
新しく作る機能だけResult型を使います。既存コードには手を付けません。
typescript// 新規API(Result型)
export async function createNewFeature(): Promise<Result<Data, AppError>> {
// Result型で実装
}
// 既存API(try-catch)はそのまま
export async function oldFeature(): Promise<Data> {
// 変更しない
}
フェーズ2: 境界層でラッパー作成
外部ライブラリや既存コードをResult型でラップします。
typescript// 既存のPromiseベースの関数をResult型に変換
function wrapPromise<T>(promise: Promise<T>): ResultAsync<T, Error> {
return ResultAsync.fromPromise(promise, (e) => e as Error);
}
// 使い方
const result = await wrapPromise(legacyAsyncFunction());
フェーズ3: クリティカルな部分をリファクタリング
エラーハンドリングが特に重要な部分(決済処理、認証など)を優先的に移行します。
この段階的なアプローチにより、チームの学習コストを抑えつつ、型安全性の恩恵を受けられました。
まとめ
Result型とNeverthrowの使い方は、TypeScriptでの型安全なエラーハンドリングを実現する有力な選択肢です。ただし、万能ではありません。
プロジェクトの規模や複雑さ、チームの経験に応じて判断してください。小規模なプロトタイプや、エラーの種類が単純な処理では、従来のtry-catchで十分なケースもあります。
一方で、中〜大規模なアプリケーション、複数人でのチーム開発、長期メンテナンスが前提のコードでは、Result型の型安全性が大きな価値を発揮します。実際にプロダクション環境で採用した結果、エラーハンドリングの漏れが減り、リファクタリングの安全性が向上しました。
導入を検討する際は、まず新規機能から小さく始め、チームで使い方を学びながら適用範囲を広げていくアプローチをお勧めします。never型やユニオン型の使い方に慣れれば、TypeScriptの型システムを最大限に活用できるはずです。
あなたのプロジェクトでも、型安全なエラーハンドリングの使い方を試してみてはいかがでしょうか。
関連リンク
著書
article2026年1月10日TypeScriptでモノレポ管理をセットアップする手順 プロジェクト分割と依存関係制御の実践
article2026年1月10日StorybookとTypeScriptのユースケース 型安全なUI開発を設計して運用する
article2026年1月9日TypeScriptプロジェクトの整形とLintをセットアップする手順 PrettierとESLintの最適構成
article2026年1月9日Vue 3とTypeScriptをセットアップして型安全に始める手順 propsとemitsの設計も整理
article2026年1月9日TypeScriptのビルド最適化を比較・検証する esbuild swc tscのベンチマークと使い分け
article2026年1月8日ESLintのparser設定を比較・検証する Babel TypeScript Flowの違いと選び方
article2026年1月10日TypeScriptでモノレポ管理をセットアップする手順 プロジェクト分割と依存関係制御の実践
article2026年1月10日StorybookとTypeScriptのユースケース 型安全なUI開発を設計して運用する
article2026年1月9日TypeScriptプロジェクトの整形とLintをセットアップする手順 PrettierとESLintの最適構成
article2026年1月9日Vue 3とTypeScriptをセットアップして型安全に始める手順 propsとemitsの設計も整理
article2026年1月9日TypeScriptのビルド最適化を比較・検証する esbuild swc tscのベンチマークと使い分け
article2026年1月8日ESLintのparser設定を比較・検証する Babel TypeScript Flowの違いと選び方
blogiPhone 17シリーズの発表!全モデルiPhone 16から進化したポイントを見やすく整理
blogGoogleストアから訂正案内!Pixel 10ポイント有効期限「1年」表示は誤りだった
blog【2025年8月】Googleストア「ストアポイント」は1年表記はミス?2年ルールとの整合性を検証
blogGoogleストアの注文キャンセルはなぜ起きる?Pixel 10購入前に知るべき注意点
blogPixcel 10シリーズの発表!全モデル Pixcel 9 から進化したポイントを見やすく整理
blogフロントエンドエンジニアの成長戦略:コーチングで最速スキルアップする方法
review今の自分に満足していますか?『持たざる者の逆襲 まだ何者でもない君へ』溝口勇児
reviewついに語られた業界の裏側!『フジテレビの正体』堀江貴文が描くテレビ局の本当の姿
review愛する勇気を持てば人生が変わる!『幸せになる勇気』岸見一郎・古賀史健のアドラー実践編で真の幸福を手に入れる
review週末を変えれば年収も変わる!『世界の一流は「休日」に何をしているのか』越川慎司の一流週末メソッド
review新しい自分に会いに行こう!『自分の変え方』村岡大樹の認知科学コーチングで人生リセット
review科学革命から AI 時代へ!『サピエンス全史 下巻』ユヴァル・ノア・ハラリが予見する人類の未来
