neverthrowを学ぶ
前提
今回はChatGPTによる解説を入れながら個人的な感想を入れた記事になります。
対象としてはneverthrowってなんだ?となったタイミングで読んでいただけますと幸いです。
僕もneverthrowってなんだ?という状況から学習していますので、一緒に読み進めていって学習していただけますと幸いです。
ちなみにちょっとRustにはまっていたこともありResultについてのキャッチアップは早かったです。
The Rust Programming Language 日本語版
公式のgithubはこちらneverthrow
1. まずは説明をよみます
説明
プログラムにエラー処理を組み込む。
このパッケージには、成功(Ok)または失敗(Err)を表すResult型が含まれています。
非同期タスクの場合、neverthrow
はResultAsync
クラスを提供しており、これはPromise<Result<T, E>>
をラップして、通常のResult<T, E>
と同じレベルの表現力と制御を提供します。
ResultAsync
はthenable
であり、ネイティブのPromise<Result>
とまったく同じように動作しますが、Promiseをawait
したり.then
したりする必要なく、Result
が提供するのと同じメソッドにアクセスできます!例やベストプラクティスについては、wikiをご覧ください。
このパッケージを使ったエラーハンドリングの実際の例が見たいですか?このリポジトリをご覧ください:https://github.com/parlez-vous/server
2. わからない用語がいっぱい出てきたので調べます
最初に翻訳した説明で出てきた
・ResultAsyncクラス
・Promise<Result<T, E>>をラップして、通常のResult<T, E>と同じレベルの表現力と制御を提供
・ResultAsyncはthenableであり
・ネイティブのPromise<Result>と全く同じように動作する
・Promiseをawaitしたり、.thenしたりする必要はなく
・Resultが提供するのと同じメソッドでアクセスできる
それぞれの意味が分からなかったので読み解いていきます。
3. Resultの基本的な考え方
neverthrow
は、Rustなどのプログラミング言語に見られるResult
型の概念をJavaScript/TypeScriptに導入します。Result
型は、操作が成功した場合の結果(Ok
)と、失敗した場合のエラー(Err
)を安全に扱うための方法です。
-
Ok
型: 操作が成功したときの結果を格納します。 -
Err
型: 操作が失敗したときのエラー情報を格納します。
Resultの型定義
export type Result<T, E> = Ok<T, E> | Err<T, E>
Result<T, E>
の型定義は、以下の理由で型の統一性、一貫性、そして柔軟性を兼ね備えています
型の統一性
-
Result<T, E>
という一つの型で、成功(Ok<T, E>
)と失敗(Err<T, E>
)の両方を扱えるように設計されています。これにより、操作の結果が成功であっても失敗であっても、常にResult<T, E>
型として統一して扱うことができます。
一貫性
-
Ok
とErr
が同じジェネリック型引数を持つことで、コード全体での型の扱いが一貫します。Result
型のインターフェースを考える際に、一貫した方法で成功とエラーの両方を処理できるため、コードの可読性やメンテナンス性が向上します。
柔軟性
- ジェネリック型引数を両方のコンポーネントに持たせることで、複雑な型やジェネリックな操作に対応しやすくなります。これは、型システムを活用した安全で再利用性の高いコードを書く上で非常に重要です。
OkとErrのクラス定義について
export class Ok<T, E> implements IResult<T, E> {
constructor(readonly value: T) {}
// 以下メソッドの定義が続きます。
}
export class Err<T, E> implements IResult<T, E> {
constructor(readonly error: E) {}
// 以下メソッドの定義が続きます。
}
Ok
クラスは、操作が成功した場合にのみ値を保持するためのクラスです。つまり、Ok
インスタンスが作成されるのは、処理が成功したときだけです。そして、このOk
インスタンスは、成功時の値を保持し、それに対する操作(メソッドの呼び出しなど)は、成功パターンに基づいて処理されます。
IResultのインターフェース
OkとErrは共通のimplements IResultが実装されています。
ですので次はこのIResultを確認していきます。
interface IResult
このインターフェース IResult<T, E>
は、Result
型に対して共通のメソッドを定義するためのものです。このインターフェースを使うことで、Ok
と Err
のクラスが同じメソッドを持つことを強制し、結果としてResult
型を使った一貫した操作が可能になります。
インターフェース IResult<T, E>
の役割
IResult<T, E>
は、Result
型を構成する Ok<T, E>
と Err<T, E>
クラスに共通するメソッドを定義しています。このインターフェースを実装することで、Ok
と Err
の両方のクラスは、以下のメソッドを持つことが保証されます。
それではインターフェースメソッドを一つずつ確認していきます。
isOk
-
isOk(): this is Ok<T, E>
という定義は、次のような意味を持ちます:-
isOk
メソッドがtrue
を返すと、そのオブジェクト(this
)はOk<T, E>
型だとTypeScriptに教えています。 - これにより、その後のコードでTypeScriptは「このオブジェクトは
Ok
型だから、Ok
型にだけ存在するプロパティやメソッドを安全に使える」と認識します。
-
このように、isOk()
メソッドがtrue
を返すことで、TypeScriptは「このオブジェクトはOk<T, E>
型だ」と理解し、その後の処理でOk
型のメソッドやプロパティを安全に扱えるようになります。
isErr
-
isErr(): this is Err<T, E>
という定義は、次のような意味を持ちます:-
isErr
メソッドがtrue
を返すと、そのオブジェクト(this
)はErr<T, E>
型だとTypeScriptに教えています。 - これにより、その後のコードでTypeScriptは「このオブジェクトは
Err
型だから、Err
型にだけ存在するプロパティやメソッドを安全に使える」と認識します。
-
このように、isErr()
メソッドがtrue
を返すことで、TypeScriptは「このオブジェクトはErr<T, E>
型だ」と理解し、その後の処理でErr
型のメソッドやプロパティを安全に扱えるようになります。
map
このメソッドは、isOk
がtrue
である、つまりthis
がOk<T, E>
である場合に、Ok
が持っている値(T
型の値)を引数として受け取り、その値をf
関数で処理して新しい型A
に変換します。その結果を新しいResult<A, E>
として返すメソッドです。
-
isOk()
がtrue
の場合:map
メソッドはOk
の値に対して処理を行い、新しいResult
を返します。 -
isOk()
がfalse
の場合:map
メソッドは何もせず、元のErr
をそのまま返します。
このように、map
メソッドはOk
の値に対してのみ処理を行うメソッドであり、エラーが発生している場合にはそのままの状態を保持します。
mapErr
このメソッドは、isErr()
がtrue
である、つまりthis
がErr<T, E>
である場合に、Err
が持っているエラー(E
型の値)を引数として受け取り、そのエラーをf
関数で処理して新しい型U
に変換します。その結果を新しいResult<T, U>
として返すメソッドです。
-
isErr()
がtrue
の場合:mapErr
メソッドはErr
のエラー情報に対して処理を行い、新しいResult
を返します。 -
isErr()
がfalse
の場合:mapErr
メソッドは何もせず、元のOk
をそのまま返します。
このように、mapErr
メソッドはエラーが発生している場合にそのエラーを処理するために使用され、成功している場合にはそのままの状態を保持するため、map
メソッドとは逆の役割を持っています。
andThen
andThen
メソッドはオーバーロード(関数の多重定義)されています。これは、同じ名前のメソッドを異なる引数や型で定義できる機能です。
andThen
メソッドのオーバーロードの目的
オーバーロードされている理由は、andThen
メソッドが異なる種類の処理に対応できるようにするためです。具体的には、次のような2つのケースに対応しています。
-
汎用的な
Result
型を返す場合:andThen<R extends Result<unknown, unknown>>( f: (t: T) => R, ): Result<InferOkTypes<R>, InferErrTypes<R> | E>
- この定義は、
f
が任意のResult
型を返す場合に対応しています。ジェネリクスR
はResult
型で、f
はT
型の値を受け取ってR
型を返す関数です。 - このバージョンの
andThen
は、複数のResult
型をネストしてしまった場合に、それらを一つのResult
型にフラット化するのに役立ちます。
- この定義は、
-
特定の型
U
とF
を返す場合:andThen<U, F>(f: (t: T) => Result<U, F>): Result<U, E | F>
- この定義は、
f
がResult<U, F>
型を返す場合に対応しています。f
はT
型の値を受け取ってResult<U, F>
型を返す関数です。 - このバージョンの
andThen
は、T
型の値を用いて次の処理を行い、その結果を新しいResult<U, F>
として返すシンプルなケースに対応しています。
- この定義は、
オーバーロードの使い分け
-
Result
型が多重にネストされる場合:- 最初の
andThen
メソッドは、Result
型が多重にネストされた場合に、それらをフラットにするために使います。例えば、Result<Result<A, E2>, E1>
をResult<A, E2>
に変換するような場面で使います。
- 最初の
-
単純な型変換が必要な場合:
- 二つ目の
andThen
メソッドは、特定の型U
とF
に変換し、新しいResult<U, F>
を返す場合に使います。これは、次の計算ステップに進みたい場合に使います。
- 二つ目の
andThen
メソッドが二つあるのは、異なるタイプの処理に対応するためのオーバーロードによるものです。これにより、同じ名前のメソッドでありながら、異なる状況に応じて適切な処理を行えるようになっています。TypeScriptでは、このようにして同じメソッド名で異なる処理をサポートすることで、より柔軟なコードを書くことが可能になります。
orElse
orElse
メソッドも多重定義されており、失敗(Err
)したときに実行するためのメソッドです。orElse
は、エラーハンドリングやエラーからの回復に役立つメソッドで、柔軟に対応できるように2つの異なるパターンが定義されています。
orElse
メソッドは、Err
が発生した場合に、そのErr
を他のResult
型に変換するために使用します。Ok
の場合は、元の値がそのまま保持され、Err
の場合だけが処理されます。
-
複雑なケース:
orElse<R extends Result<unknown, unknown>>(f: (e: E) => R): Result<T, InferErrTypes<R>>
- この定義は、
f
が複雑な型のResult
を返す場合に対応します。f
関数はE
型のエラーを受け取り、新しいResult
を返します。この場合、InferErrTypes<R>
が新しいエラー型を抽出し、結果としてResult<T, InferErrTypes<R>>
が返されます。 - これは、より柔軟で複雑なエラーハンドリングをサポートします。
- この定義は、
-
単純なケース:
orElse<A>(f: (e: E) => Result<T, A>): Result<T, A>
- この定義は、単純に
E
型のエラーを別の型A
に変換し、新しいResult<T, A>
を返す場合に使用します。f
関数はE
型のエラーを受け取り、Result<T, A>
を返します。 - 典型的なエラーハンドリングのパターンに使用されます。
- この定義は、単純に
-
複雑なケース:
orElse
メソッドの最初のオーバーロードは、より柔軟で複雑なエラーハンドリングを可能にします。複数のResult
型が絡む場合に役立ちます。 - 単純なケース: 2番目のオーバーロードは、シンプルなエラー処理に適しており、特定のエラーを別の値やエラーに変換する場合に使用します。
orElse
メソッドは、失敗(Err
)時に柔軟に対応できるよう設計されており、エラーハンドリングのパターンを整理しやすくします。
asyncAndThen
asyncAndThen
メソッドは、非同期の処理を連続して行う場合に使用されるメソッドです。andThen
メソッドの非同期バージョンと考えるとわかりやすいです。
- 目的: 非同期処理を連続して行うために使用します。特に、ある非同期の処理が成功した場合に次の非同期処理を実行し、その結果を返すという流れを作るために使います。
-
動作:
asyncAndThen
メソッドは、Result
がOk
である場合に、非同期の関数f
を使ってその中の値を処理します。f
はT
型の値を受け取り、ResultAsync<U, F>
型を返します。
asyncAndThen<U, F>(f: (t: T) => ResultAsync<U, F>): ResultAsync<U, E | F>
-
U
とF
:-
U
は次の非同期処理が成功した場合の新しい型です。 -
F
は次の非同期処理が失敗した場合のエラー型です。
-
-
引数
f
:- 関数
f
はT
型の値を受け取り、非同期に処理を行ってResultAsync<U, F>
を返します。
- 関数
-
戻り値:
-
asyncAndThen
は、新しいResultAsync<U, E | F>
を返します。このResultAsync
は非同期に処理を行い、結果が得られるまで待機します。
-
-
**
asyncAndThen
**は、非同期処理を連続して行うためのメソッドです。andThen
メソッドが同期処理を連続させるのに対し、asyncAndThen
は非同期処理を連続させます。 -
非同期処理のチェーンを作りたい場合、特に各ステップが非同期であり、その結果に基づいて次の非同期処理を行う必要がある場合に非常に有用です。
このメソッドを使うことで、非同期処理を安全かつ整然とした形で連続して実行し、エラーハンドリングも含めて一貫した処理を行うことができます。
andThenとasyncAndThenで混乱してきたので以下をよみました!!
同期処理と非同期処理の違い
-
同期処理 (Synchronous Processing):
- 結果を待ってから次のステップに進みます。つまり、ある操作が完了するまでプログラムの流れが停止し、次のステップに進むのはその操作が完了してからです。
-
例:
andThen
は同期処理です。andThen
を使った場合、その操作が完了して結果が得られるまで次の処理に進みません。
-
非同期処理 (Asynchronous Processing):
- 結果を待たずに次のステップに進みます。ただし、結果が返ってきたらその結果を使って次の操作が実行されるように設定します。非同期処理では、時間のかかる操作(例: ネットワーク呼び出し)を待たずにプログラムの他の部分が続行されます。
-
例:
asyncAndThen
は非同期処理です。asyncAndThen
を使った場合、その操作が完了するまで次の操作は行われませんが、プログラム自体は他のことを行うことができます。
andThen
)
同期処理 (const result = someFunction().andThen(value => nextFunction(value));
console.log("この行は、andThenの処理が終わってから実行されます");
-
流れ:
someFunction
が完了し、その結果が次のnextFunction
に渡されます。andThen
の処理が終わるまで、次の行(console.log
)には進みません。
asyncAndThen
)
非同期処理 (const result = someAsyncFunction().asyncAndThen(value => nextAsyncFunction(value));
console.log("この行は、asyncAndThenの処理中でもすぐに実行されます");
-
流れ:
someAsyncFunction
が非同期で実行され、その結果が返ってくるのを待ってnextAsyncFunction
が呼ばれます。しかし、プログラムはその間も他のことを行います(ここではconsole.log
がすぐに実行される)。
まとめ
-
同期処理: 結果を待ってから次に進む。
andThen
がこれに該当します。 -
非同期処理: 結果を待たずに次に進み、結果が返ってきたらその結果を使って次の処理を行う。
asyncAndThen
がこれに該当します。
非同期処理は、特にネットワーク呼び出しやファイル入出力のような時間のかかる操作に対して有効です。プログラムの他の部分をブロックせずに、これらの操作を行うことができます。
ResultAsyncのクラス
その通りです!ここまでの情報を簡潔にまとめます。
ResultAsync
クラス
-
役割:
ResultAsync
は非同期処理の結果をResult
(Ok
またはErr
)としてラップし、エラーハンドリングを容易にするクラスです。Promise
のように使えますが、Result
を通じてエラーや成功の処理を一貫して行います。 -
static
メソッド:ResultAsync
クラスにはstatic
メソッド(例:fromSafePromise
,fromPromise
)が定義されており、これらをクラス名から直接呼び出すことができます。これにより、インスタンスを作成せずに非同期処理を簡単に開始できます。-
例:
ResultAsync.fromSafePromise(promise)
のように、static
メソッドを使って非同期処理をラップします。
-
例:
asyncAndThen
メソッド
-
役割:
asyncAndThen
はResultAsync
インスタンスメソッドで、非同期処理が成功(Ok
)した場合に次の非同期処理を続け、失敗(Err
)した場合はエラーをそのまま伝搬します。 -
型情報の管理:
asyncAndThen
では、次の非同期処理が成功する場合と失敗する場合の型情報(Ok
とErr
の型)を適切に管理します。失敗時のエラー型は、元のエラー型と新しいエラー型の両方を考慮します(例:ResultAsync<U, E | F>
)。 -
メソッドチェーン:
asyncAndThen
を使うことで、非同期処理を連続してチェーンしつつ、それぞれのステップで成功と失敗の処理を行えます。
まとめ
-
ResultAsync
クラスは、非同期処理の結果をラップするクラスで、エラーハンドリングをシンプルにします。static
メソッドを使って直接呼び出せます。 -
asyncAndThen
メソッドは、ResultAsync
のインスタンスメソッドで、非同期処理を連続して行い、成功時には次の処理を行い、失敗時にはエラーを伝搬します。 - これにより、非同期処理の流れを管理し、各ステップでのエラーハンドリングを一貫して行えるようになります。
この仕組みを活用することで、複雑な非同期処理を効率的に、かつ型安全に扱うことができます。
補足:static
具体的にまとめると
-
static
を宣言したメソッドやプロパティ:- クラスを実体化することなく使用可能: これらはクラスのインスタンス(オブジェクト)に属さず、クラス自体に属します。したがって、クラス名を通じて直接呼び出すことができます。
-
使用例:
class MyClass { static myStaticMethod() { console.log("This is a static method"); } } MyClass.myStaticMethod(); // "This is a static method"
- この例では、
MyClass
をインスタンス化する必要はなく、クラス名を使ってmyStaticMethod
を直接呼び出しています。
-
static
を宣言していないメソッドやプロパティ:- クラスを実体化(インスタンス化)しないと使用できない: これらはクラスのインスタンスに属するため、インスタンスが生成されない限りアクセスできません。
-
使用例:
class MyClass { myInstanceMethod() { console.log("This is an instance method"); } } const myInstance = new MyClass(); myInstance.myInstanceMethod(); // "This is an instance method"
- この例では、
MyClass
をインスタンス化して、myInstance
オブジェクトを生成した後にmyInstanceMethod
を呼び出しています。
まとめ
-
static
メソッド/プロパティ:- クラスに直接属し、インスタンスを生成せずに使用できる。
- クラス名を使ってアクセスする。
-
インスタンスメソッド/プロパティ:
- クラスのインスタンスに属し、インスタンスを生成しないと使用できない。
- インスタンスを通じてアクセスする。
static
メソッドは、インスタンスの状態に依存しない汎用的な処理を提供する場合に非常に便利です。一方で、インスタンスメソッドは、クラスのインスタンスごとに異なる動作やデータを扱う必要がある場合に使用します。
補足:impliments
implements
は、TypeScriptでクラスがインターフェースを実装する際に使われるキーワードです。
implements
の役割
-
インターフェースを実装する: クラスがインターフェースを
implements
するということは、そのクラスがインターフェースで定義されたメソッドやプロパティをすべて実装しなければならないという意味です。 -
型安全性の確保: インターフェースを
implements
することで、クラスがそのインターフェースの契約に従っていることを保証し、型安全性を確保します。
implements
の例
例えば、次のようなインターフェースがあるとします。
interface MyInterface {
myMethod(param: string): void;
}
このインターフェースをimplements
するクラスは、MyInterface
で定義されたmyMethod
を実装しなければなりません。
class MyClass implements MyInterface {
myMethod(param: string): void {
console.log(param);
}
}
このように、MyClass
はMyInterface
をimplements
することで、myMethod
を必ず実装することが強制されます。
ResultAsync
クラスでのimplements
あなたが提示したコードでは、ResultAsync
クラスがPromiseLike<Result<T, E>>
をimplements
しています。これは、ResultAsync
クラスがPromiseLike
インターフェースを実装しており、少なくともそのインターフェースが要求するthen
メソッドを持っていることを意味します。
具体的には、ResultAsync
クラスは次のような役割を果たします。
-
PromiseLike<Result<T, E>>
を実装することで、ResultAsync
クラスはPromise
のように扱うことができ、.then()
を使った非同期処理のチェーンに組み込むことができます。
まとめ
implements
キーワードは、クラスが特定のインターフェースを実装することを示します。これにより、クラスがそのインターフェースの契約を遵守し、型安全性を保ちながら、指定されたメソッドやプロパティを実装することを保証します。ResultAsync
クラスがPromiseLike
をimplements
していることで、ResultAsync
はPromise
のように扱えるクラスとなります。
補足:PromiseLike
ResultAsyncがPromiseではなくPromiseLikeを使用している理由
ResultAsync
がPromise
ではなくPromiseLike
を実装している理由はいくつかあります。
1. 柔軟性の確保
-
PromiseLike
のインターフェースを実装することで、ResultAsync
クラスはPromise
と互換性を持ちながらも、完全にPromise
に依存しない設計が可能になります。 -
ResultAsync
はPromise
に似た振る舞いをしますが、独自のメソッド(map
,andThen
,mapErr
など)を持っており、Promise
クラスそのものの動作を完全に覆うことなく、必要な機能だけを提供できます。
2. 独自機能の追加
-
ResultAsync
はResult
の機能を組み合わせた非同期処理を扱うクラスです。これには、Result
の特性(Ok
やErr
など)を保持しつつ、非同期処理の流れを管理するという目的があります。 -
PromiseLike
を実装することで、then
メソッドを備えつつ、通常のPromise
とは異なる振る舞いや独自のメソッドを追加できるため、ResultAsync
の特性を保ちながら非同期処理を行えます。
3. 互換性と拡張性
- **
PromiseLike
**はPromise
と互換性があります。つまり、PromiseLike
を実装することで、ResultAsync
は他のPromise
と同じように使うことができますが、Promise
クラスの完全な実装をする必要はありません。 -
ResultAsync
クラスは、Promise
の一部の機能だけを持ち、必要な独自機能を追加することで、Promise
の持つ制約から自由になります。これにより、ResultAsync
クラスはより特化した非同期処理を行うための柔軟な設計が可能です。
4. TypeScriptの型システムとの親和性
-
PromiseLike
を使うことで、TypeScriptの型システムを最大限に活用しつつ、ResultAsync
のインターフェースをより軽量に保つことができます。 - また、
PromiseLike
はthen
メソッドだけを要求するため、ResultAsync
はPromise
に依存することなく、必要な部分だけを実装できます。これにより、軽量で効率的な非同期処理クラスを作ることができます。
まとめ
ResultAsync
がPromiseLike
を実装する理由は、Promise
と互換性を持ちながらも、Promise
に縛られずに独自の機能を持たせるためです。これにより、ResultAsync
はPromise
の利便性を享受しつつ、Result
の特性を生かした非同期処理を柔軟に行うことができます。
asyncMap
asyncMap
メソッドの概要
-
役割:
asyncMap
は、Result<T, E>
のOk
値を非同期関数で変換し、ResultAsync<U, E>
を返します。 -
動作:
-
Ok
の場合: 非同期関数を使って値を変換し、新しいResultAsync<U, E>
を返します。 -
Err
の場合: そのままErr
を保持し、変換は行われません。
-
使用例
const result = ResultAsync.fromSafePromise(Promise.resolve(42))
.asyncMap(async (value) => {
return value * 2; // 非同期で値を変換
});
result.match({
ok: (newValue) => console.log(newValue), // 84
err: (error) => console.error(error),
});
まとめ
-
asyncMap
メソッドは、Ok
の値を非同期処理で変換するために使用します。 -
Err
はそのまま保持され、変換されません。 - 非同期処理を含む値の変換が簡単に行えます。
unwrapOr
unwrapOr
メソッドは、ResultAsync
のOk
値を取得するためのメソッドで、もしErr
の場合は指定したデフォルト値を返すというものです。以下にその特徴を簡潔にまとめます。
unwrapOr
メソッドの役割
-
目的:
Ok
値を取得するか、もしResultAsync
がErr
の場合はデフォルト値を返すために使用します。
メソッドのシグネチャ
unwrapOr<A>(v: A): T | A
-
T
:ResultAsync
が持つ成功時の型です。 -
A
:Err
の場合に返すデフォルト値の型です。 -
戻り値:
ResultAsync
がOk
の場合はその値(T
)、Err
の場合はデフォルト値(A
)を返します。
動作の流れ
-
Ok
のとき:ResultAsync
がOk
を持っている場合、そのOk
の値を返します。 -
Err
のとき:ResultAsync
がErr
の場合、引数で指定したデフォルト値を返します。
使用例
const result = ResultAsync.fromSafePromise(Promise.resolve(42));
const value = result.unwrapOr(0); // Okの場合は42、Errの場合は0を返す
console.log(value); // 42
別の例では、Err
が発生した場合にデフォルト値を返します。
const result = ResultAsync.fromSafePromise(Promise.reject("Error"));
const value = result.unwrapOr(0); // Okの場合はその値、Errの場合は0を返す
console.log(value); // 0
まとめ
-
unwrapOr
メソッドは、Ok
の値を取り出すか、Err
の場合はデフォルト値を返すためのメソッドです。 -
簡単にエラーハンドリング:
Err
の場合でも、安全にデフォルト値を返すことで、エラー処理をシンプルにします。 - 使いやすさ: 非同期処理の結果がエラーであっても、プログラムの流れを中断させずに、代替の値を返すことができるため、柔軟なコードを実現します。
match
match
メソッドは、Result
オブジェクトがOk
かErr
かに応じて、対応する関数を実行し、その結果を返すためのメソッドです。このメソッドを使用することで、Result
の状態に基づいて異なる処理を行うことができます。以下にその特徴をまとめます。
match
メソッドの役割
-
目的:
Result
がOk
の場合とErr
の場合に応じて、それぞれ異なる関数を実行し、その結果を返すために使用します。
メソッドのシグネチャ
match<A, B = A>(ok: (t: T) => A, err: (e: E) => B): A | B
-
T
:Ok
の値の型です。 -
E
:Err
の値の型です。 -
A
:Ok
の場合に実行される関数が返す値の型です。 -
B
:Err
の場合に実行される関数が返す値の型です(省略時はA
と同じ型になります)。 -
戻り値:
Ok
の場合はA
型の値、Err
の場合はB
型の値が返されます。
動作の流れ
-
Ok
の場合:ok
コールバック関数が実行され、その結果が返されます。 -
Err
の場合:err
コールバック関数が実行され、その結果が返されます。
使用例
const result = ResultAsync.fromSafePromise(Promise.resolve(42));
const message = result.match(
(value) => `Success: ${value}`, // Okの場合の処理
(error) => `Error: ${error}` // Errの場合の処理
);
console.log(message); // "Success: 42"
別の例では、Err
が発生した場合の処理も行います。
const result = ResultAsync.fromSafePromise(Promise.reject("Something went wrong"));
const message = result.match(
(value) => `Success: ${value}`, // Okの場合の処理
(error) => `Error: ${error}` // Errの場合の処理
);
console.log(message); // "Error: Something went wrong"
match
の利点
-
シンプルなエラーハンドリング:
match
を使うことで、Ok
とErr
の両方のケースを一つのメソッドで処理できます。 -
統一された戻り値の型:
map
やmapErr
と異なり、match
では両方の関数が同じ戻り値の型を持つ必要があります。これにより、戻り値の型が一致することが保証されます。 -
柔軟な処理:
match
を使うことで、Result
の内容に応じた柔軟な処理を簡潔に記述できます。
まとめ
-
match
メソッドは、Result
の状態に応じてOk
またはErr
の処理を行い、その結果を返します。 -
統一された戻り値:
match
は、Ok
とErr
で異なる処理を行いながらも、同じ型の戻り値を返すことができます。 -
使いやすさ:
Result
の状態に応じた処理をシンプルに記述でき、エラーハンドリングや成功時の処理を一貫して行うことができます。
safeUnwrap
safeUnwrap
メソッドは、Rustの?
演算子に似た動作をエミュレートし、安全にResult
のOk
値を取り出すためのメソッドです。もしResult
がErr
の場合は、そのエラーをジェネレーターとして返します。以下に、その特徴をまとめます。
safeUnwrap
メソッドの役割
-
目的:
Result
がOk
の場合はその値を取り出し、Err
の場合はそのエラーをジェネレーターとして返すために使用します。これは、Rustの?
演算子のように、エラーが発生した場合にその場で処理を中断し、エラーを返すことを可能にします。
メソッドのシグネチャ
safeUnwrap(): Generator<Err<never, E>, T>
-
T
:Ok
の値の型です。 -
E
:Err
のエラーの型です。 -
戻り値:
Generator<Err<never, E>, T>
、Ok
の場合はT
を返し、Err
の場合はErr<never, E>
をジェネレーターとして返します。
動作の流れ
-
Ok
の場合:-
Ok
の値が取り出され、ジェネレーターはその値を返します。
-
-
Err
の場合:- ジェネレーターは
Err
を返し、処理を中断します。
- ジェネレーターは
使用例
function* example() {
const result = ResultAsync.fromSafePromise(Promise.resolve(42));
const value = yield* result.safeUnwrap(); // Okの場合は42が返る
console.log(value); // 42
}
const generator = example();
generator.next(); // 実行して結果を取り出す
別の例では、Err
が発生した場合の処理も行います。
function* example() {
const result = ResultAsync.fromSafePromise(Promise.reject("Something went wrong"));
const value = yield* result.safeUnwrap(); // Errの場合はここで処理が中断され、エラーが返る
console.log(value); // 実行されない
}
const generator = example();
const { value, done } = generator.next();
if (!done) {
console.error(value); // エラーメッセージ "Something went wrong"
}
safeUnwrap
の利点
-
簡潔なエラーハンドリング:
safeUnwrap
を使うことで、Result
のエラーが発生した場合に簡潔に処理を中断し、エラーを返すことができます。 -
Rust風のエラーハンドリング: Rustの
?
演算子に似た動作を実現でき、エラーハンドリングがシンプルになります。
まとめ
-
safeUnwrap
メソッドは、Result
のOk
値を安全に取り出し、Err
の場合はエラーをジェネレーターとして返すメソッドです。 -
使いやすさ: Rustの
?
演算子に似たエラーハンドリングをTypeScriptでも実現できます。 - ジェネレーターとの連携: このメソッドを使うことで、処理の中断やエラーハンドリングがジェネレーターを通じてスムーズに行えます。
補足 ジェネレーターについて
ジェネレーター関数とジェネレーター式についての概要
ジェネレーター関数とジェネレーター式は、JavaScript/TypeScriptにおける特殊な関数の一種で、関数の実行を途中で一時停止し、外部からの指示で再開することができる機能を持っています。これにより、複雑なイテレーションや非同期処理をシンプルに扱うことができます。
function*
)
1. ジェネレーター関数 (-
定義方法:
function*
キーワードを使って定義します。通常の関数と異なり、yield
キーワードを使って実行を一時停止し、再開することができます。 -
シンタックス:
function* myGenerator() { yield 1; yield 2; yield 3; }
-
特徴:
-
一時停止と再開:
yield
キーワードを使って関数の実行を一時停止し、外部から再開することができます。 -
イテレータを返す: ジェネレーター関数を呼び出すと、イテレータオブジェクトが返され、そのイテレータの
next
メソッドを使って値を順に取り出せます。
-
一時停止と再開:
-
使用例:
function* numberGenerator() { yield 1; yield 2; yield 3; } const generator = numberGenerator(); console.log(generator.next().value); // 1 console.log(generator.next().value); // 2 console.log(generator.next().value); // 3 console.log(generator.next().done); // true (すべての値を生成したので完了)
yield
キーワード
2. -
役割: ジェネレーター関数内で、値を外部に返しつつ、関数の実行を一時停止します。
next()
が呼ばれると、前回のyield
で停止した場所から実行が再開されます。 -
特徴:
-
値の生成:
yield
は、ジェネレーター関数が返す値を生成し、外部に渡します。 -
制御の移譲: ジェネレーターの外部に制御を渡し、次の
next()
呼び出しまで関数の実行を停止します。
-
値の生成:
yield*
キーワード
3. -
役割: 別のジェネレーター関数や反復可能オブジェクト(配列、文字列など)を展開して、そのすべての要素を順に生成します。
-
特徴:
-
委譲:
yield*
を使うと、別のジェネレーターや反復可能なオブジェクトに制御を委譲し、その要素を順に返します。
-
委譲:
-
使用例:
function* subGenerator() { yield 3; yield 4; } function* mainGenerator() { yield 1; yield 2; yield* subGenerator(); // ここでsubGeneratorの要素を順に生成 yield 5; } const generator = mainGenerator(); console.log(generator.next().value); // 1 console.log(generator.next().value); // 2 console.log(generator.next().value); // 3 console.log(generator.next().value); // 4 console.log(generator.next().value); // 5
4. ジェネレーター式
-
定義方法: ジェネレーター関数の匿名関数バージョンです。
function*
キーワードを使って、無名関数としてジェネレーターを定義できます。 -
使用例:
const myGenerator = function* () { yield 1; yield 2; yield 3; };
まとめ
-
ジェネレーター関数 (
function*
): 関数の実行を一時停止し、再開することができる特殊な関数。yield
で値を順に生成し、next()
で次の値を取得します。 -
yield
キーワード: ジェネレーター関数の実行を一時停止し、外部に値を返すために使用します。 -
yield*
キーワード: 別のジェネレーターや反復可能オブジェクトを展開して、その要素を順に生成するために使用します。 - ジェネレーター式: ジェネレーター関数の無名バージョンで、匿名関数として定義できます。
ジェネレーターを使うことで、複雑なイテレーションや非同期処理を直感的かつ簡潔に記述することができます。
ジェネレーターとPromiseは、どちらもJavaScript/TypeScriptで非同期処理や逐次処理を扱うために使用される機能ですが、役割や動作は異なります。以下にそれぞれの特徴と違いをまとめます。
Generator
)
ジェネレーター(-
主な用途: ジェネレーターは逐次処理を制御するために使われます。関数の実行を途中で一時停止し、外部から再開させることができます。
-
動作:
- ジェネレーター関数(
function*
)を使用して定義されます。 -
yield
キーワードで処理を一時停止し、値を外部に返します。 -
next()
メソッドを使ってジェネレーターの実行を再開し、次のyield
まで実行されます。 -
yield*
キーワードを使って、他のジェネレーターや反復可能なオブジェクトに処理を委譲することができます。
- ジェネレーター関数(
-
特徴:
- ジェネレーターは「同期的な」処理フローの制御に使われますが、非同期処理と組み合わせて使うこともあります。
- ジェネレーターはイテレーターを返し、そのイテレーターを使って順次処理を行うことができます。
-
例:
function* myGenerator() { yield 1; yield 2; yield 3; } const gen = myGenerator(); console.log(gen.next().value); // 1 console.log(gen.next().value); // 2 console.log(gen.next().value); // 3
Promise
)
プロミス(-
主な用途: Promiseは非同期処理の結果を扱うために使われます。非同期処理が完了する前に結果を得ることができない場合に利用します。
-
動作:
-
Promise
オブジェクトは、非同期処理が成功(resolve
)したか失敗(reject
)したかを表します。 -
.then()
メソッドを使って、非同期処理が成功した場合の処理を定義できます。 -
.catch()
メソッドを使って、非同期処理が失敗した場合のエラーハンドリングを行います。 -
.finally()
メソッドを使って、非同期処理が完了した後の処理を定義できます。
-
-
特徴:
- Promiseは「非同期的な」処理の完了を待つために使います。
- 非同期処理のチェーンを作成することができ、複数の非同期処理を順に行うことが容易になります。
-
例:
const myPromise = new Promise((resolve, reject) => { setTimeout(() => resolve("Success!"), 1000); }); myPromise .then(result => console.log(result)) // "Success!" (1秒後) .catch(error => console.error(error));
ジェネレーターとプロミスの違い
-
目的:
- ジェネレーターは逐次処理の流れを制御するために使われ、同期的な処理や反復処理のカスタマイズが得意です。
- Promiseは非同期処理の完了を待つために使われ、非同期のエラーハンドリングやチェーン処理が得意です。
-
動作:
- ジェネレーターは関数を一時停止し、再開することができます。
- Promiseは非同期処理が完了したら、結果を受け取ることができます。
-
使用場面:
- ジェネレーターは複雑な逐次処理、特にイテレーションやコルーチンに適しています。
- Promiseは非同期のAPI呼び出し、タイマー、ファイル操作などに適しています。
-
構文:
-
ジェネレーター:
function*
,yield
,yield*
-
Promise:
new Promise
,.then()
,.catch()
,.finally()
-
ジェネレーター:
まとめ
- ジェネレーターは、処理の流れを細かく制御でき、主に逐次処理やイテレーションに使われます。
- Promiseは、非同期処理の結果を扱うためのツールで、非同期処理が完了した時に何をするかを定義できます。
両者は異なる目的で使用されますが、組み合わせることで、強力な非同期処理と逐次処理のコントロールを実現することもできます。
_unsafeUnwrap
_unsafeUnwrap
メソッドは、Result<T, E>
オブジェクトからOk
の値を強制的に取り出すためのメソッドです。ただし、Result
がErr
の場合にはカスタムエラーオブジェクトをスローするため、非常に危険であり、通常はテスト環境でのみ使用されるべきものです。以下に、このメソッドの特徴をまとめます。
_unsafeUnwrap
メソッドの概要
-
役割:
Result
がOk
である場合に値を強制的に取り出し、Err
である場合は例外をスローするためのメソッドです。安全ではないため、通常のアプリケーションコードではなく、テスト環境での使用が推奨されます。
メソッドのシグネチャ
_unsafeUnwrap(config?: ErrorConfig): T
-
T
:Result
のOk
状態に含まれる値の型です。 -
config
(オプション): エラーが発生した際に、カスタムエラーオブジェクトをスローするための設定情報を含むオプションのパラメータです。
動作の流れ
-
Ok
の場合:-
Result
がOk
の状態であれば、その値を直接返します。
-
-
Err
の場合:-
Result
がErr
の状態であれば、設定されたカスタムオブジェクト(config
)を含むエラーをスローします。
-
使用例
const result = new Ok<number, string>(42);
try {
const value = result._unsafeUnwrap(); // 42を返す
console.log(value);
} catch (error) {
console.error(error); // `Err`の場合は例外が発生する
}
別の例では、Err
が発生した場合のカスタムエラー処理を行います。
const result = new Err<number, string>("Something went wrong");
try {
const value = result._unsafeUnwrap(); // 例外が発生し、キャッチされる
} catch (error) {
console.error("Caught error:", error); // "Caught error: Something went wrong"
}
_unsafeUnwrap
の利点と注意点
-
利点:
-
簡単なテスト: テスト環境で、エラーが発生しないことを前提にしたコードを簡単に記述できるため、テスト中に期待通りの
Ok
値が返されることを確認できます。
-
簡単なテスト: テスト環境で、エラーが発生しないことを前提にしたコードを簡単に記述できるため、テスト中に期待通りの
-
注意点:
-
危険性: このメソッドは
Err
が発生したときに即座に例外をスローするため、通常のアプリケーションコードで使用するのは非常に危険です。エラーハンドリングが不十分な場合、予期しない例外が発生してアプリケーションがクラッシュする可能性があります。 - 使用すべき環境: ドキュメントにも記載されている通り、このメソッドは主にテスト環境での使用に限られます。生産環境での使用は避けるべきです。
-
危険性: このメソッドは
まとめ
-
_unsafeUnwrap
メソッドは、Result
がOk
であればその値を取り出し、Err
であればカスタムエラーをスローする、非常に危険なメソッドです。 - 主にテスト環境で使用: このメソッドは、通常のアプリケーションコードではなく、テスト環境でエラーが発生しないことを前提にした処理を行うために使用されます。
-
使用にあたっての注意:
Err
が発生した場合に例外がスローされるため、適切なエラーハンドリングがないと予期しないクラッシュを引き起こす可能性があります。
補足unsafeとの比較
TypeScriptの_unsafeUnwrap
メソッドは、Rustのunsafe
ブロックやunsafe
関数と似た概念ですが、いくつか重要な違いがあります。ここでは、その類似点と違いを説明します。
unsafe
Rustの-
役割: Rustの
unsafe
は、通常のRustの安全性保証を無効にして、危険な操作を行うことを許可するキーワードです。例えば、未定義動作を引き起こす可能性があるポインタ操作や、メモリの手動管理、外部関数呼び出し(FFI)などが該当します。 -
使用例:
unsafe { // ここでは安全性が保証されない操作が行われる some_unsafe_function(); }
-
リスク:
unsafe
ブロック内のコードは、通常のRustの所有権、借用、型システムによる安全性チェックの一部が無効化されるため、バグやメモリの安全性の問題が生じやすくなります。
_unsafeUnwrap
TypeScriptの-
役割: TypeScriptの
_unsafeUnwrap
は、Result
がErr
の場合でも強制的に値を取り出そうとするメソッドです。失敗すると例外がスローされますが、Rustのunsafe
とは異なり、言語レベルでの安全性チェックが関わるわけではありません。 -
使用例:
const result = new Ok<number, string>(42); try { const value = result._unsafeUnwrap(); // Okの値を返す console.log(value); } catch (error) { console.error("Caught an error:", error); }
-
リスク: このメソッドは、
Err
の状態を無視して強制的に値を取得しようとするため、予期しない例外を引き起こす可能性があり、通常のアプリケーションコードでの使用は危険です。ただし、TypeScriptにはRustのようなメモリ管理や未定義動作のリスクがないため、unsafe
とは異なる意味での「危険性」となります。
類似点
- 安全性の欠如: どちらも「通常は安全な操作が保証されているが、特定の状況ではそれを無視してリスクを取る」という点で類似しています。
- 慎重な使用: どちらも慎重に使う必要があり、特に通常のコードではなく、特定のテストや低レベルの操作を行う場合に使用されるべきです。
違い
-
レベルの違い:
- Rustの
unsafe
は、言語レベルでのメモリの安全性や未定義動作に直接関わる非常に低レベルな操作を許可します。 - TypeScriptの
_unsafeUnwrap
は、例外処理の管理が主な焦点であり、メモリ管理や未定義動作のリスクはありません。
- Rustの
-
目的の違い:
-
Rustの
unsafe
: 高度なパフォーマンスチューニングやシステムプログラミングなど、必要不可欠な場合に限って使用されます。 -
_unsafeUnwrap
: テストや特定のシナリオでエラーハンドリングをスキップしたい場合に使用されます。
-
Rustの
まとめ
- **
_unsafeUnwrap
**は、Rustのunsafe
に似た名前を持つメソッドですが、実際にはメモリ管理や未定義動作のリスクがないため、意味合いが異なります。 -
慎重な使用: 両者ともに、通常の安全なプログラミング慣習を逸脱する操作を行うため、慎重に使用する必要があります。
_unsafeUnwrap
は、エラーを無視して強制的に値を取得するため、予期しない例外が発生するリスクがあります。
_unSafeUnWrapErr
_unsafeUnwrapErr(config?: ErrorConfig): E
は、Result
がErr
の場合、そのエラー値を強制的に取り出すメソッドです。しかし、Result
がOk
の場合はカスタムエラーオブジェクトをスローします。このメソッドも通常のアプリケーションコードでは危険であり、主にテスト環境での使用が推奨されます。
ポイント
-
Err
の場合: エラー値を返します。 -
Ok
の場合: エラーをスローします。 - 使用用途: テスト環境でのみ使用することが推奨されます。
そもそもこれは何がうれしいのか?
neverthrow
は、JavaScriptやTypeScriptでエラーハンドリングをより安全かつ簡単に行うためのライブラリです。特に、コードの中で「例外(エラー)」が発生する可能性がある処理を行う際に便利です。
IResultのインターフェースも確認できたのであとは練習して使い方を学んでいきます。
つづきは別の記事にて学習していきます!
補足:RustのResult型
RustのResult型について
RustのResult
型は、操作の成功と失敗を明確に扱うためのデータ型です。これは特にエラーハンドリングの際に使用され、プログラムの安全性を高めるために非常に役立ちます。
Result
型の基本構造
Result
型は次のように定義されています:
enum Result<T, E> {
Ok(T),
Err(E),
}
-
T
: 成功した場合に返される値の型。 -
E
: 失敗した場合に返されるエラーの型。
Result
型は2つのバリアント(列挙型の値)を持っています:
-
Ok(T)
: 操作が成功した場合、このバリアントが選択され、T
型の値を保持します。 -
Err(E)
: 操作が失敗した場合、このバリアントが選択され、E
型のエラー情報を保持します。
例
例えば、ファイルを開く操作を行うとき、ファイルが存在しない場合や権限がない場合にエラーが発生する可能性があります。このような場合、Result
型を使用してエラーハンドリングを行います。
use std::fs::File;
use std::io::{self, Read};
fn read_file(filename: &str) -> Result<String, io::Error> {
let mut file = File::open(filename)?; // ファイルを開く、失敗すればErrが返る
let mut contents = String::new();
file.read_to_string(&mut contents)?; // ファイル内容を読み取る、失敗すればErrが返る
Ok(contents) // 成功すればOkが返る
}
fn main() {
match read_file("example.txt") {
Ok(contents) => println!("File contents: {}", contents),
Err(e) => println!("Failed to read the file: {}", e),
}
}
コードの説明
-
read_file
関数:-
引数:
filename
としてファイル名(文字列)を受け取ります。 -
戻り値:
Result<String, io::Error>
を返します。成功した場合はファイルの内容(String
)を返し、失敗した場合はio::Error
型のエラーを返します。 -
処理の流れ:
-
File::open(filename)
を呼び出して、指定されたファイルを開こうとします。ファイルのオープンに成功するとfile
にFile
オブジェクトが格納されますが、失敗するとErr(io::Error)
を返します。 -
file.read_to_string(&mut contents)
を使って、ファイルの内容をcontents
というString
型の変数に読み込みます。この操作も、成功すればOk(())
を返し、失敗すればErr(io::Error)
を返します。 - 最終的に、ファイルの内容を
Ok(contents)
として返します。
-
-
引数:
-
main
関数:-
read_file("example.txt")
を呼び出して、example.txt
というファイルを読み込みます。 -
match
式を使って、read_file
の結果をOk
かErr
に分岐して処理します。-
Ok(contents)
の場合:contents
にファイルの内容が格納されており、それをprintln!
で出力します。 -
Err(e)
の場合: ファイルの読み込みに失敗した場合で、エラー内容をe
として受け取り、そのエラーメッセージをprintln!
で出力します。
-
-
サンプルコードのまとめ
このコードは、ファイルを開いてその内容を読み込む処理を行い、読み込みが成功したか失敗したかに応じて適切なメッセージを出力するというものです。Result
型を使うことで、ファイル操作が成功した場合と失敗した場合の両方をしっかりとハンドリングしています。
Result
型の活用方法
-
エラーハンドリングを強制する: Rustでは
Result
型を返す関数を呼び出すとき、必ずその結果を処理しなければなりません。これにより、エラーを見逃すリスクを減らすことができます。 -
match
式で分岐処理:Result
型の値に対してmatch
式を使い、Ok
とErr
の場合で異なる処理を行うことができます。 -
?
演算子の使用:?
演算子を使うと、Result
型がErr
の場合に即座に関数から抜け出し、呼び出し元にエラーを返すことができます。Ok
の場合はその値を自動的に展開します。これにより、エラーハンドリングコードが簡潔になります。
Result
型を使うのか
なぜResult
型を使うことで、エラーが発生したときの対応を強制的に行わせることができ、プログラムの堅牢性が向上します。Rustは例外をスローしない言語であり、Result
型を使ってすべてのエラーを明示的に扱うことで、予期せぬエラーによるクラッシュを防ぐことができます。これにより、安全で信頼性の高いコードを書くことが可能になります。
Rustとneverthrowの違い
neverthrow
のResult
はクラスとして定義されていますが、RustのResult
は型として定義されています。この違いは、各言語の特性と設計思想に基づいていますが、基本的な役割や機能は似ています。
-
Rustの
Result
: Rustでは、Result
は型(列挙型)であり、型システムの一部として動作します。これは、コンパイラによって厳密にチェックされる静的な型システムを活用して、安全なエラーハンドリングを実現しています。 -
neverthrow
のResult
:neverthrow
では、Result
はクラスとして定義されており、JavaScript/TypeScriptの柔軟な環境に合わせて設計されています。TypeScriptの型システムを使って、RustのResult
に似たエラーハンドリングを可能にしています。
どちらもエラーハンドリングを強化するための仕組みですが、それぞれの言語の特性に応じて設計されているという点で異なります。
Discussion