Result型使ってますか?
\スニダンを開発しているSODA inc.の Advent Calendar 2024 6日目の記事です!!!/
こんにちは、昨日と同じく私Mapleの記事です。
「Result型使ってますか?」解説していきます!
TypeScriptでのエラーハンドリングは、従来のtry-catch
構文を用いる方法が一般的です。しかし、関数の戻り値としてエラーを扱うResult型を使用することで、より安全で明確なコードを書くことができます。
本記事では、Result型のメリットや、try-catch
と比較した際の優位性について詳しく解説します。
1. Result型とは
Result型は、操作が成功したか失敗したかを明示的に表す型です。
以下の2つの状態を持ちます。
- 成功(Ok):期待された結果が得られた場合。
- 失敗(Err):エラー情報を含む場合。
Result型の定義例
type Result<T, E> = Ok<T> | Err<E>;
interface Ok<T> {
isOk: true;
value: T;
}
interface Err<E> {
isOk: false;
error: E;
}
2. Result型のメリット
2.1 明示的なエラーハンドリング
関数の戻り値にエラーが含まれる可能性を明示的に示すことができます。
function parseJSON<T>(input: string): Result<T, Error> {
try {
const parsed = JSON.parse(input);
return { isOk: true, value: parsed };
} catch (e) {
return { isOk: false, error: e };
}
}
2.2 型安全性の向上
Result型を用いると、コンパイラがエラーハンドリングの漏れを検出できます。これは、開発者がエラー処理を忘れるリスクを減少させます。
const result = parseJSON<{ name: string }>('{"name": "Alice"}');
if (result.isOk) {
console.log(result.value.name);
} else {
console.error('パースに失敗しました:', result.error);
}
2.3 非同期処理との相性の良さ
非同期処理でのエラーハンドリングも、Result型を使うことで一貫性を保てます。
async function fetchData(url: string): Promise<Result<Data, Error>> {
try {
const response = await fetch(url);
const data = await response.json();
return { isOk: true, value: data };
} catch (e) {
return { isOk: false, error: e };
}
}
2.4 例外の予測可能性
try-catch
では、関数内部で予期せぬ例外が発生する可能性がありますが、Result型を使用すると、エラーが戻り値として扱われるため、例外の伝播を制御できます。
3. try-catchとの比較
3.1 try-catchの課題
- 暗黙的なエラー処理:エラーが発生するかどうかが分かりにくい。
-
非同期処理での複雑性:
async/await
と組み合わせると、try-catch
がネストしがち。
function parseJSON<T>(input: string): T {
return JSON.parse(input);
}
try {
const data = parseJSON<{ name: string }>('{"name": "Alice"}');
console.log(data.name);
} catch (e) {
console.error('パースに失敗しました:', e);
}
3.2 Result型の優位性
- 明示的なエラー情報:関数の戻り値でエラーを扱うため、エラー処理が明確。
- コンポーザビリティ:エラー処理を一元管理できる。
- テストの容易性:テストがしやすい。
4. Result型を用いた関数の組み合わせ
Result型を使うと、複数の関数を組み合わせる際のエラーハンドリングがシンプルになります。
function validate(data: any): Result<ValidatedData, ValidationError> {
// バリデーション処理
}
function save(data: ValidatedData): Result<SavedData, SaveError> {
// データ保存処理
}
const result = parseJSON(rawInput)
.then(validate)
.then(save);
if (result.isOk) {
console.log('データ保存に成功しました:', result.value);
} else {
console.error('エラーが発生しました:', result.error);
}
5. エラーハンドリングの例
5.1 パターンマッチング風の処理
TypeScriptの型ガードを用いて、Result型の状態に応じた処理ができます。
function isOk<T, E>(result: Result<T, E>): result is Ok<T> {
return result.isOk;
}
if (isOk(result)) {
// 成功時の処理
} else {
// エラー時の処理
}
5.2 unwrap関数の使用
安全にResult型の値を取り出す関数を作成。
function unwrap<T, E>(result: Result<T, E>): T {
if (result.isOk) {
return result.value;
} else {
throw result.error;
}
}
try {
const data = unwrap(result);
console.log(data);
} catch (e) {
console.error('エラーが発生しました:', e);
}
6. まとめ
-
Result型のメリット
- エラー処理が明示的になり、コードの可読性と安全性が向上。
- 型システムを活用して、エラーハンドリングの漏れを防止。
- 関数の合成や非同期処理との組み合わせが容易。
-
try-catchのデメリット
- エラーが関数シグネチャに現れず、暗黙的なエラー処理となる。
- 非同期処理でのネストが深くなり、コードが複雑化。
- 型安全性が低く、エラーハンドリングの漏れをコンパイラが検出できない。
**Result型を用いることで、メンテナンス性の高いコードを書くことが可能だと考えています!
**TypeScriptの型システムを最大限に活用し、エラーハンドリングを明示的かつ効率的に行いましょう!
株式会社SODAの開発組織がお届けするZenn Publicationです。 是非Entrance Bookもご覧ください! → recruit.soda-inc.jp/engineer
Discussion