はじめに#
TypeScriptを使っていても、エラーハンドリングの部分で「型安全性」が失われていることに気づいていますか。try-catchでキャッチしたエラーはunknown型となり、どのようなエラーが投げられるかは型システムから見えません。これはTypeScriptの大きな盲点の一つです。
本記事では、TypeScriptにおけるエラーハンドリングの課題を明らかにし、それを解決するための実践的なパターンを解説します。カスタムエラークラスによる型の明確化、Result型パターンによる関数型アプローチ、そしてneverthrowライブラリを使った型安全なエラーハンドリングまで、堅牢なアプリケーションを構築するための知識を体系的に学べます。
この記事を読み終えると、以下のことができるようになります。
- TypeScriptにおける
try-catchの型安全性の問題を理解できる
- カスタムエラークラスを設計し、エラーを型で区別できる
- Result型パターンでエラーを戻り値として明示的に扱える
- neverthrowライブラリを使って型安全なエラーハンドリングを実装できる
実行環境・前提条件#
前提知識#
動作確認環境#
| ツール |
バージョン |
| Node.js |
20.x以上 |
| TypeScript |
5.7以上 |
| neverthrow |
8.x |
| VS Code |
最新版 |
本記事のサンプルコードは、TypeScript Playgroundで動作確認できます。neverthrowを使用するコードは、ローカル環境でのインストールが必要です。
期待される結果#
本記事のコードを実行すると、以下の動作を確認できます。
- カスタムエラークラスで型安全にエラーを判別できる
- Result型によってエラーの可能性が型シグネチャに明示される
- neverthrowを使ったチェーン処理でエラーを宣言的に扱える
TypeScriptにおけるtry-catchの問題点#
catchブロックのerrorはunknown型#
TypeScriptでは、catchブロックで受け取るエラーはunknown型です。これは型安全性の観点から正しい設計ですが、開発者にとっては扱いにくい側面があります。
1
2
3
4
5
6
7
8
9
10
11
|
async function fetchUser(id: string) {
try {
const response = await fetch(`/api/users/${id}`);
return await response.json();
} catch (error) {
// error は unknown 型
// error.message とは書けない
console.error(error);
throw error;
}
}
|
unknown型のエラーにアクセスするには、型ガードやアサーションが必要になります。
1
2
3
4
5
6
7
8
9
10
|
try {
// 何らかの処理
} catch (error) {
if (error instanceof Error) {
console.error(error.message); // OK: string
console.error(error.stack); // OK: string | undefined
} else {
console.error("Unknown error:", error);
}
}
|
関数シグネチャにエラー情報がない#
TypeScriptの関数シグネチャには、その関数がどのようなエラーを投げる可能性があるかという情報がありません。Javaのthrows宣言のような仕組みがないのです。
1
2
3
4
5
6
7
|
// この関数がどんなエラーを投げるか、型からは分からない
function parseJSON(text: string): unknown {
return JSON.parse(text); // SyntaxError を投げる可能性がある
}
// 呼び出し側は try-catch で囲むべきか判断できない
const data = parseJSON('{"name": "John"}');
|
これは以下の問題を引き起こします。
- エラーハンドリングの漏れ: 開発者がエラーの可能性に気づかない
- ドキュメント依存: JSDocコメントなど外部の情報に頼る必要がある
- リファクタリングの困難: エラーの種類が変わっても型チェックで検出できない
非同期処理でのエラー追跡の難しさ#
Promiseのリジェクトも型情報を持ちません。
1
2
3
4
5
6
7
8
|
// Promise<User> という型だが、リジェクトの型は不明
async function getUser(id: string): Promise<User> {
const response = await fetch(`/api/users/${id}`);
if (!response.ok) {
throw new Error(`HTTP ${response.status}`);
}
return response.json();
}
|
flowchart TD
A["async function getUser(): Promise<User>"] --> B{処理}
B -->|成功| C["Promise.resolve(user)"]
B -->|失敗| D["Promise.reject(???)"]
C --> E["型: User"]
D --> F["型: unknown(情報なし)"]カスタムエラークラスによる型安全なエラー#
カスタムエラークラスの基本#
JavaScriptのErrorクラスを継承して、アプリケーション固有のエラークラスを作成できます。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
|
class ValidationError extends Error {
constructor(
message: string,
public readonly field: string,
public readonly value: unknown
) {
super(message);
this.name = "ValidationError";
// プロトタイプチェーンを正しく設定(TypeScriptのES5トランスパイル対策)
Object.setPrototypeOf(this, ValidationError.prototype);
}
}
class NotFoundError extends Error {
constructor(
public readonly resource: string,
public readonly id: string
) {
super(`${resource} with id ${id} not found`);
this.name = "NotFoundError";
Object.setPrototypeOf(this, NotFoundError.prototype);
}
}
class NetworkError extends Error {
constructor(
message: string,
public readonly statusCode?: number,
public readonly cause?: Error
) {
super(message);
this.name = "NetworkError";
Object.setPrototypeOf(this, NetworkError.prototype);
}
}
|
instanceofによる型判別#
カスタムエラークラスはinstanceofで判別でき、TypeScriptは自動的に型を絞り込みます。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
|
function handleError(error: unknown): void {
if (error instanceof ValidationError) {
// この分岐では error は ValidationError 型
console.error(`Validation failed for field "${error.field}": ${error.message}`);
console.error(`Invalid value:`, error.value);
} else if (error instanceof NotFoundError) {
// この分岐では error は NotFoundError 型
console.error(`${error.resource} not found: ${error.id}`);
} else if (error instanceof NetworkError) {
// この分岐では error は NetworkError 型
console.error(`Network error (${error.statusCode}): ${error.message}`);
if (error.cause) {
console.error("Caused by:", error.cause);
}
} else if (error instanceof Error) {
console.error("Unexpected error:", error.message);
} else {
console.error("Unknown error:", error);
}
}
|
エラー階層の設計#
大規模なアプリケーションでは、エラークラスを階層構造にすると管理しやすくなります。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
|
// 基底クラス
abstract class AppError extends Error {
abstract readonly code: string;
readonly timestamp: Date;
constructor(message: string) {
super(message);
this.timestamp = new Date();
Object.setPrototypeOf(this, new.target.prototype);
}
}
// ドメインエラー
class DomainError extends AppError {
readonly code = "DOMAIN_ERROR";
}
class UserNotFoundError extends DomainError {
readonly code = "USER_NOT_FOUND";
constructor(public readonly userId: string) {
super(`User ${userId} not found`);
}
}
class InvalidEmailError extends DomainError {
readonly code = "INVALID_EMAIL";
constructor(public readonly email: string) {
super(`Invalid email format: ${email}`);
}
}
// インフラエラー
class InfrastructureError extends AppError {
readonly code = "INFRASTRUCTURE_ERROR";
}
class DatabaseConnectionError extends InfrastructureError {
readonly code = "DB_CONNECTION_ERROR";
constructor(message: string, public readonly host: string) {
super(message);
}
}
|
classDiagram
Error <|-- AppError
AppError <|-- DomainError
AppError <|-- InfrastructureError
DomainError <|-- UserNotFoundError
DomainError <|-- InvalidEmailError
InfrastructureError <|-- DatabaseConnectionError
class AppError {
<<abstract>>
+code: string
+timestamp: Date
}
class DomainError {
+code: "DOMAIN_ERROR"
}
class UserNotFoundError {
+code: "USER_NOT_FOUND"
+userId: string
}カスタムエラーの限界#
カスタムエラークラスには改善点がありますが、根本的な問題は解決されません。
- 関数シグネチャにエラー情報を含められない
throwはプログラムの制御フローを中断する
- 複数のエラーを合成的に扱いにくい
これらの問題を解決するのがResult型パターンです。
Result型パターンの基本#
Result型とは#
Result型は、関数型プログラミング言語でよく使われるパターンで、処理の成功と失敗を戻り値として表現します。RustやHaskellなどの言語では標準的な手法です。
1
2
3
|
type Result<T, E> =
| { success: true; value: T }
| { success: false; error: E };
|
これにより、エラーの可能性が型シグネチャに明示されます。
1
2
3
4
5
6
7
8
9
10
11
12
13
|
function divide(a: number, b: number): Result<number, string> {
if (b === 0) {
return { success: false, error: "Division by zero" };
}
return { success: true, value: a / b };
}
const result = divide(10, 2);
if (result.success) {
console.log(`Result: ${result.value}`); // result.value は number
} else {
console.log(`Error: ${result.error}`); // result.error は string
}
|
シンプルなResult型の実装#
より実用的なResult型を実装してみます。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
|
// 成功を表す型
type Ok<T> = {
readonly _tag: "Ok";
readonly value: T;
};
// 失敗を表す型
type Err<E> = {
readonly _tag: "Err";
readonly error: E;
};
// Result型(判別可能なユニオン型)
type Result<T, E> = Ok<T> | Err<E>;
// ヘルパー関数
function ok<T>(value: T): Ok<T> {
return { _tag: "Ok", value };
}
function err<E>(error: E): Err<E> {
return { _tag: "Err", error };
}
// 型ガード
function isOk<T, E>(result: Result<T, E>): result is Ok<T> {
return result._tag === "Ok";
}
function isErr<T, E>(result: Result<T, E>): result is Err<E> {
return result._tag === "Err";
}
|
Result型を使った関数の実装#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
|
interface User {
id: string;
name: string;
email: string;
}
interface ParseError {
type: "PARSE_ERROR";
message: string;
}
interface ValidationError {
type: "VALIDATION_ERROR";
field: string;
message: string;
}
type UserParseError = ParseError | ValidationError;
function parseUser(json: string): Result<User, UserParseError> {
// JSONパース
let data: unknown;
try {
data = JSON.parse(json);
} catch {
return err({
type: "PARSE_ERROR",
message: "Invalid JSON format"
});
}
// バリデーション
if (typeof data !== "object" || data === null) {
return err({
type: "VALIDATION_ERROR",
field: "root",
message: "Expected an object"
});
}
const obj = data as Record<string, unknown>;
if (typeof obj.id !== "string") {
return err({
type: "VALIDATION_ERROR",
field: "id",
message: "id must be a string"
});
}
if (typeof obj.name !== "string") {
return err({
type: "VALIDATION_ERROR",
field: "name",
message: "name must be a string"
});
}
if (typeof obj.email !== "string") {
return err({
type: "VALIDATION_ERROR",
field: "email",
message: "email must be a string"
});
}
return ok({
id: obj.id,
name: obj.name,
email: obj.email
});
}
|
Result型のメリット#
flowchart LR
subgraph "従来のtry-catch"
A1["function(): T"]
A2["throw Error"]
A1 -.->|"暗黙的"| A2
end
subgraph "Result型"
B1["function(): Result<T, E>"]
B2["Ok<T> | Err<E>"]
B1 -->|"明示的"| B2
end
- 型シグネチャの明確化: エラーの可能性が戻り値の型に含まれる
- 強制的なエラーハンドリング: 戻り値を使うには成功・失敗の判定が必要
- 合成可能性: 複数のResultを組み合わせやすい
- 制御フローの明確化:
throwによる非ローカルジャンプがない
neverthrowライブラリの活用#
neverthrowとは#
neverthrowは、TypeScript向けのResult型実装を提供するライブラリです。7,000以上のGitHubスターを獲得し、多くのプロジェクトで採用されています。
基本的な使い方#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
|
import { ok, err, Result } from "neverthrow";
interface User {
id: string;
name: string;
}
interface FetchError {
type: "NETWORK_ERROR" | "NOT_FOUND" | "PARSE_ERROR";
message: string;
}
function fetchUser(id: string): Result<User, FetchError> {
// 仮のロジック
if (id === "invalid") {
return err({
type: "NOT_FOUND",
message: `User ${id} not found`
});
}
return ok({
id,
name: "John Doe"
});
}
const result = fetchUser("123");
// isOk / isErr でチェック
if (result.isOk()) {
console.log(`User: ${result.value.name}`);
} else {
console.log(`Error: ${result.error.message}`);
}
|
mapとmapErrによる変換#
mapメソッドで成功値を変換し、mapErrでエラーを変換できます。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
|
import { ok, err, Result } from "neverthrow";
interface RawUser {
id: string;
first_name: string;
last_name: string;
}
interface User {
id: string;
fullName: string;
}
function fetchRawUser(id: string): Result<RawUser, string> {
if (id === "invalid") {
return err("User not found");
}
return ok({ id, first_name: "John", last_name: "Doe" });
}
// mapで成功値を変換
const userResult = fetchRawUser("123")
.map((raw): User => ({
id: raw.id,
fullName: `${raw.first_name} ${raw.last_name}`
}))
.mapErr((error) => ({
type: "FETCH_ERROR" as const,
message: error
}));
// userResult は Result<User, { type: "FETCH_ERROR"; message: string }> 型
|
andThenによるチェーン処理#
複数のResult型を返す関数を連鎖させる場合はandThenを使います。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
|
import { ok, err, Result } from "neverthrow";
interface User {
id: string;
name: string;
}
interface Order {
id: string;
userId: string;
total: number;
}
interface AppError {
code: string;
message: string;
}
function getUser(id: string): Result<User, AppError> {
if (id === "invalid") {
return err({ code: "USER_NOT_FOUND", message: "User not found" });
}
return ok({ id, name: "John" });
}
function getOrders(userId: string): Result<Order[], AppError> {
if (userId === "blocked") {
return err({ code: "USER_BLOCKED", message: "User is blocked" });
}
return ok([
{ id: "order-1", userId, total: 100 },
{ id: "order-2", userId, total: 200 }
]);
}
function calculateTotal(orders: Order[]): Result<number, AppError> {
const total = orders.reduce((sum, order) => sum + order.total, 0);
if (total > 10000) {
return err({ code: "LIMIT_EXCEEDED", message: "Order limit exceeded" });
}
return ok(total);
}
// チェーン処理
const totalResult = getUser("user-123")
.andThen((user) => getOrders(user.id))
.andThen((orders) => calculateTotal(orders));
// 結果の処理
totalResult.match(
(total) => console.log(`Total: ${total}`),
(error) => console.log(`Error [${error.code}]: ${error.message}`)
);
|
flowchart TD
A["getUser('user-123')"] -->|Ok| B["getOrders(user.id)"]
A -->|Err| E["Error: USER_NOT_FOUND"]
B -->|Ok| C["calculateTotal(orders)"]
B -->|Err| F["Error: USER_BLOCKED"]
C -->|Ok| D["Success: total"]
C -->|Err| G["Error: LIMIT_EXCEEDED"]matchによるパターンマッチング#
matchメソッドで成功と失敗の両方のケースを処理できます。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
import { ok, err, Result } from "neverthrow";
function divide(a: number, b: number): Result<number, string> {
if (b === 0) {
return err("Division by zero");
}
return ok(a / b);
}
// match で両方のケースを処理
const message = divide(10, 2).match(
(value) => `Result: ${value}`,
(error) => `Error: ${error}`
);
console.log(message); // "Result: 5"
|
ResultAsyncによる非同期処理#
neverthrowは非同期処理用のResultAsyncも提供しています。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
|
import { okAsync, errAsync, ResultAsync, fromPromise } from "neverthrow";
interface User {
id: string;
name: string;
}
interface ApiError {
code: string;
message: string;
}
// Promise を ResultAsync に変換
function fetchUser(id: string): ResultAsync<User, ApiError> {
return fromPromise(
fetch(`/api/users/${id}`).then((res) => {
if (!res.ok) {
throw new Error(`HTTP ${res.status}`);
}
return res.json() as Promise<User>;
}),
(error): ApiError => ({
code: "FETCH_ERROR",
message: error instanceof Error ? error.message : "Unknown error"
})
);
}
// ResultAsync もチェーン可能
async function main() {
const result = await fetchUser("123")
.map((user) => user.name.toUpperCase())
.mapErr((error) => `Failed: ${error.message}`);
result.match(
(name) => console.log(`User: ${name}`),
(error) => console.error(error)
);
}
main();
|
Result.fromThrowableでtry-catchをラップ#
既存の例外を投げる関数をResult型に変換できます。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
|
import { Result } from "neverthrow";
interface ParseError {
type: "PARSE_ERROR";
message: string;
}
// JSON.parse を Result を返す関数に変換
const safeJsonParse = Result.fromThrowable(
JSON.parse,
(error): ParseError => ({
type: "PARSE_ERROR",
message: error instanceof Error ? error.message : "Unknown parse error"
})
);
const result1 = safeJsonParse('{"name": "John"}');
// Result<unknown, ParseError>
const result2 = safeJsonParse("invalid json");
// Err({ type: "PARSE_ERROR", message: "..." })
result1.match(
(data) => console.log("Parsed:", data),
(error) => console.error("Error:", error.message)
);
|
Result.combineで複数のResultを合成#
複数のResultを一度に処理する場合はResult.combineを使います。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
|
import { ok, err, Result } from "neverthrow";
interface ValidationError {
field: string;
message: string;
}
function validateName(name: string): Result<string, ValidationError> {
if (name.length < 2) {
return err({ field: "name", message: "Name must be at least 2 characters" });
}
return ok(name);
}
function validateEmail(email: string): Result<string, ValidationError> {
if (!email.includes("@")) {
return err({ field: "email", message: "Invalid email format" });
}
return ok(email);
}
function validateAge(age: number): Result<number, ValidationError> {
if (age < 0 || age > 150) {
return err({ field: "age", message: "Invalid age" });
}
return ok(age);
}
// 全てのバリデーションを実行
const combined = Result.combine([
validateName("John"),
validateEmail("john@example.com"),
validateAge(30)
]);
// combined は Result<[string, string, number], ValidationError> 型
combined.match(
([name, email, age]) => {
console.log(`Valid user: ${name}, ${email}, ${age}`);
},
(error) => {
console.error(`Validation failed: ${error.field} - ${error.message}`);
}
);
|
型安全なエラーハンドリングのベストプラクティス#
エラー型の設計指針#
エラー型は以下の点を考慮して設計します。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
|
// 1. 判別可能なユニオン型を使う
type AppError =
| { type: "VALIDATION_ERROR"; field: string; message: string }
| { type: "NOT_FOUND"; resource: string; id: string }
| { type: "NETWORK_ERROR"; statusCode: number; message: string }
| { type: "UNAUTHORIZED"; message: string };
// 2. エラーコードは定数で管理
const ErrorCodes = {
VALIDATION_ERROR: "VALIDATION_ERROR",
NOT_FOUND: "NOT_FOUND",
NETWORK_ERROR: "NETWORK_ERROR",
UNAUTHORIZED: "UNAUTHORIZED"
} as const;
type ErrorCode = (typeof ErrorCodes)[keyof typeof ErrorCodes];
// 3. 基底インターフェースを定義
interface BaseError {
type: ErrorCode;
message: string;
timestamp: Date;
}
|
レイヤーごとのエラー変換#
アプリケーションの各レイヤーでエラーを適切に変換します。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
|
import { Result, ResultAsync, err, ok, fromPromise } from "neverthrow";
// インフラ層のエラー
type InfraError =
| { type: "DB_ERROR"; message: string }
| { type: "NETWORK_ERROR"; message: string };
// ドメイン層のエラー
type DomainError =
| { type: "USER_NOT_FOUND"; userId: string }
| { type: "INVALID_OPERATION"; message: string };
// プレゼンテーション層のエラー
type PresentationError = {
code: string;
message: string;
statusCode: number;
};
// インフラ層
function fetchFromDatabase(id: string): ResultAsync<unknown, InfraError> {
return fromPromise(
Promise.resolve({ id, name: "John" }), // 仮のDB呼び出し
(): InfraError => ({ type: "DB_ERROR", message: "Connection failed" })
);
}
// ドメイン層(インフラエラーをドメインエラーに変換)
function getUser(id: string): ResultAsync<{ id: string; name: string }, DomainError> {
return fetchFromDatabase(id)
.mapErr((): DomainError => ({
type: "USER_NOT_FOUND",
userId: id
}))
.andThen((data) => {
if (typeof data === "object" && data !== null && "name" in data) {
return ok(data as { id: string; name: string });
}
return err<{ id: string; name: string }, DomainError>({
type: "INVALID_OPERATION",
message: "Invalid user data"
});
});
}
// プレゼンテーション層(ドメインエラーをAPIエラーに変換)
function toApiError(error: DomainError): PresentationError {
switch (error.type) {
case "USER_NOT_FOUND":
return {
code: "USER_NOT_FOUND",
message: `User ${error.userId} not found`,
statusCode: 404
};
case "INVALID_OPERATION":
return {
code: "BAD_REQUEST",
message: error.message,
statusCode: 400
};
}
}
|
flowchart TD
subgraph "プレゼンテーション層"
P1["PresentationError"]
P2["statusCode, code, message"]
end
subgraph "ドメイン層"
D1["DomainError"]
D2["USER_NOT_FOUND, INVALID_OPERATION"]
end
subgraph "インフラ層"
I1["InfraError"]
I2["DB_ERROR, NETWORK_ERROR"]
end
I1 --> D1
D1 --> P1網羅性チェックによる安全性#
never型を使って、すべてのエラーケースを処理していることを保証します。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
|
type AppError =
| { type: "VALIDATION_ERROR"; message: string }
| { type: "NOT_FOUND"; message: string }
| { type: "SERVER_ERROR"; message: string };
function handleError(error: AppError): string {
switch (error.type) {
case "VALIDATION_ERROR":
return `Validation failed: ${error.message}`;
case "NOT_FOUND":
return `Not found: ${error.message}`;
case "SERVER_ERROR":
return `Server error: ${error.message}`;
default:
// 全てのケースを処理していれば、ここには到達しない
const exhaustiveCheck: never = error;
return exhaustiveCheck;
}
}
|
新しいエラー型を追加した場合、switch文でそのケースを処理しないとコンパイルエラーになります。
safeTryによる簡潔な記述#
neverthrowのsafeTryを使うと、ジェネレーター構文でより読みやすいコードが書けます。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
|
import { ok, err, Result, safeTry } from "neverthrow";
function validateName(name: string): Result<string, string> {
if (name.length < 2) return err("Name too short");
return ok(name);
}
function validateAge(age: number): Result<number, string> {
if (age < 0) return err("Invalid age");
return ok(age);
}
function createUser(name: string, age: number): Result<{ name: string; age: number }, string> {
return safeTry(function* () {
const validName = yield* validateName(name);
const validAge = yield* validateAge(age);
return ok({ name: validName, age: validAge });
});
}
const result = createUser("John", 30);
result.match(
(user) => console.log(`Created: ${user.name}, ${user.age}`),
(error) => console.error(`Error: ${error}`)
);
|
まとめ#
本記事では、TypeScriptにおける型安全なエラーハンドリングについて解説しました。
学んだこと#
- try-catchの問題点:
catchブロックのunknown型、関数シグネチャにエラー情報がない問題
- カスタムエラークラス:
instanceofによる型判別、エラー階層の設計
- Result型パターン: 成功と失敗を戻り値で表現する関数型アプローチ
- neverthrowライブラリ:
map、andThen、matchによるチェーン処理、ResultAsyncによる非同期対応
使い分けの指針#
| 手法 |
適した場面 |
| try-catch + カスタムエラー |
外部ライブラリとの境界、シンプルなエラー処理 |
| Result型(自前実装) |
軽量なプロジェクト、学習目的 |
| neverthrow |
本格的なアプリケーション、複雑なエラーフロー |
型安全なエラーハンドリングを実践することで、エラーの可能性が型レベルで明示され、ハンドリングの漏れをコンパイル時に検出できます。これにより、実行時エラーを減らし、より堅牢なアプリケーションを構築できます。
参考リンク#