😄

neverthrowを学ぶ

2024/08/25に公開

前提

今回はChatGPTによる解説を入れながら個人的な感想を入れた記事になります。
対象としてはneverthrowってなんだ?となったタイミングで読んでいただけますと幸いです。

僕もneverthrowってなんだ?という状況から学習していますので、一緒に読み進めていって学習していただけますと幸いです。
ちなみにちょっとRustにはまっていたこともありResultについてのキャッチアップは早かったです。

The Rust Programming Language 日本語版

公式のgithubはこちらneverthrow

1. まずは説明をよみます

説明
プログラムにエラー処理を組み込む。

このパッケージには、成功(Ok)または失敗(Err)を表すResult型が含まれています。

非同期タスクの場合、neverthrowResultAsyncクラスを提供しており、これはPromise<Result<T, E>>をラップして、通常のResult<T, E>と同じレベルの表現力と制御を提供します。

ResultAsyncthenableであり、ネイティブの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の型定義

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>型として統一して扱うことができます。

一貫性

  • OkErrが同じジェネリック型引数を持つことで、コード全体での型の扱いが一貫します。Result型のインターフェースを考える際に、一貫した方法で成功とエラーの両方を処理できるため、コードの可読性やメンテナンス性が向上します。

柔軟性

  • ジェネリック型引数を両方のコンポーネントに持たせることで、複雑な型やジェネリックな操作に対応しやすくなります。これは、型システムを活用した安全で再利用性の高いコードを書く上で非常に重要です。


OkとErrのクラス定義について


Ok class

export class Ok<T, E> implements IResult<T, E> {
  constructor(readonly value: T) {}
  // 以下メソッドの定義が続きます。
}

Err class

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型に対して共通のメソッドを定義するためのものです。このインターフェースを使うことで、OkErr のクラスが同じメソッドを持つことを強制し、結果としてResult型を使った一貫した操作が可能になります。

インターフェース IResult<T, E> の役割

IResult<T, E> は、Result型を構成する Ok<T, E>Err<T, E> クラスに共通するメソッドを定義しています。このインターフェースを実装することで、OkErr の両方のクラスは、以下のメソッドを持つことが保証されます。

それではインターフェースメソッドを一つずつ確認していきます。


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

このメソッドは、isOktrueである、つまりthisOk<T, E>である場合に、Okが持っている値(T型の値)を引数として受け取り、その値をf関数で処理して新しい型Aに変換します。その結果を新しいResult<A, E>として返すメソッドです。

  • isOk()trueの場合: mapメソッドはOkの値に対して処理を行い、新しいResultを返します。
  • isOk()falseの場合: mapメソッドは何もせず、元のErrをそのまま返します。

このように、mapメソッドはOkの値に対してのみ処理を行うメソッドであり、エラーが発生している場合にはそのままの状態を保持します。


mapErr

このメソッドは、isErr()trueである、つまりthisErr<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つのケースに対応しています。

  1. 汎用的なResult型を返す場合:

    andThen<R extends Result<unknown, unknown>>(
      f: (t: T) => R,
    ): Result<InferOkTypes<R>, InferErrTypes<R> | E>
    
    • この定義は、fが任意のResult型を返す場合に対応しています。ジェネリクスRResult型で、fT型の値を受け取ってR型を返す関数です。
    • このバージョンのandThenは、複数のResult型をネストしてしまった場合に、それらを一つのResult型にフラット化するのに役立ちます。
  2. 特定の型UFを返す場合:

    andThen<U, F>(f: (t: T) => Result<U, F>): Result<U, E | F>
    
    • この定義は、fResult<U, F>型を返す場合に対応しています。fT型の値を受け取ってResult<U, F>型を返す関数です。
    • このバージョンのandThenは、T型の値を用いて次の処理を行い、その結果を新しいResult<U, F>として返すシンプルなケースに対応しています。

オーバーロードの使い分け

  • Result型が多重にネストされる場合:

    • 最初のandThenメソッドは、Result型が多重にネストされた場合に、それらをフラットにするために使います。例えば、Result<Result<A, E2>, E1>Result<A, E2>に変換するような場面で使います。
  • 単純な型変換が必要な場合:

    • 二つ目のandThenメソッドは、特定の型UFに変換し、新しいResult<U, F>を返す場合に使います。これは、次の計算ステップに進みたい場合に使います。

andThenメソッドが二つあるのは、異なるタイプの処理に対応するためのオーバーロードによるものです。これにより、同じ名前のメソッドでありながら、異なる状況に応じて適切な処理を行えるようになっています。TypeScriptでは、このようにして同じメソッド名で異なる処理をサポートすることで、より柔軟なコードを書くことが可能になります。


orElse

orElseメソッドも多重定義されており、失敗(Err)したときに実行するためのメソッドです。orElseは、エラーハンドリングやエラーからの回復に役立つメソッドで、柔軟に対応できるように2つの異なるパターンが定義されています。

orElseメソッドは、Errが発生した場合に、そのErrを他のResult型に変換するために使用します。Okの場合は、元の値がそのまま保持され、Errの場合だけが処理されます。

  1. 複雑なケース:

    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>>が返されます。
    • これは、より柔軟で複雑なエラーハンドリングをサポートします。
  2. 単純なケース:

    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メソッドは、ResultOkである場合に、非同期の関数fを使ってその中の値を処理します。fT型の値を受け取り、ResultAsync<U, F>型を返します。
asyncAndThen<U, F>(f: (t: T) => ResultAsync<U, F>): ResultAsync<U, E | F>
  • UF:

    • Uは次の非同期処理が成功した場合の新しい型です。
    • Fは次の非同期処理が失敗した場合のエラー型です。
  • 引数f:

    • 関数fT型の値を受け取り、非同期に処理を行って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は非同期処理の結果をResultOkまたはErr)としてラップし、エラーハンドリングを容易にするクラスです。Promiseのように使えますが、Resultを通じてエラーや成功の処理を一貫して行います。

  • staticメソッド: ResultAsyncクラスにはstaticメソッド(例: fromSafePromise, fromPromise)が定義されており、これらをクラス名から直接呼び出すことができます。これにより、インスタンスを作成せずに非同期処理を簡単に開始できます。

    • : ResultAsync.fromSafePromise(promise)のように、staticメソッドを使って非同期処理をラップします。

asyncAndThenメソッド

  • 役割: asyncAndThenResultAsyncインスタンスメソッドで、非同期処理が成功(Ok)した場合に次の非同期処理を続け、失敗(Err)した場合はエラーをそのまま伝搬します。

  • 型情報の管理: asyncAndThenでは、次の非同期処理が成功する場合と失敗する場合の型情報(OkErrの型)を適切に管理します。失敗時のエラー型は、元のエラー型と新しいエラー型の両方を考慮します(例: ResultAsync<U, E | F>)。

  • メソッドチェーン: asyncAndThenを使うことで、非同期処理を連続してチェーンしつつ、それぞれのステップで成功と失敗の処理を行えます。

まとめ

  • ResultAsyncクラスは、非同期処理の結果をラップするクラスで、エラーハンドリングをシンプルにします。staticメソッドを使って直接呼び出せます。
  • asyncAndThenメソッドは、ResultAsyncのインスタンスメソッドで、非同期処理を連続して行い、成功時には次の処理を行い、失敗時にはエラーを伝搬します。
  • これにより、非同期処理の流れを管理し、各ステップでのエラーハンドリングを一貫して行えるようになります。

この仕組みを活用することで、複雑な非同期処理を効率的に、かつ型安全に扱うことができます。

補足:static

具体的にまとめると

  1. staticを宣言したメソッドやプロパティ:

    • クラスを実体化することなく使用可能: これらはクラスのインスタンス(オブジェクト)に属さず、クラス自体に属します。したがって、クラス名を通じて直接呼び出すことができます。
    • 使用例:
      class MyClass {
        static myStaticMethod() {
          console.log("This is a static method");
        }
      }
      
      MyClass.myStaticMethod(); // "This is a static method"
      
    • この例では、MyClassをインスタンス化する必要はなく、クラス名を使ってmyStaticMethodを直接呼び出しています。
  2. 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);
  }
}

このように、MyClassMyInterfaceimplementsすることで、myMethodを必ず実装することが強制されます。

ResultAsyncクラスでのimplements

あなたが提示したコードでは、ResultAsyncクラスがPromiseLike<Result<T, E>>implementsしています。これは、ResultAsyncクラスがPromiseLikeインターフェースを実装しており、少なくともそのインターフェースが要求するthenメソッドを持っていることを意味します。

具体的には、ResultAsyncクラスは次のような役割を果たします。

  • PromiseLike<Result<T, E>>を実装することで、ResultAsyncクラスはPromiseのように扱うことができ、.then()を使った非同期処理のチェーンに組み込むことができます。

まとめ

implementsキーワードは、クラスが特定のインターフェースを実装することを示します。これにより、クラスがそのインターフェースの契約を遵守し、型安全性を保ちながら、指定されたメソッドやプロパティを実装することを保証します。ResultAsyncクラスがPromiseLikeimplementsしていることで、ResultAsyncPromiseのように扱えるクラスとなります。

補足:PromiseLike

ResultAsyncがPromiseではなくPromiseLikeを使用している理由

ResultAsyncPromiseではなくPromiseLikeを実装している理由はいくつかあります。

1. 柔軟性の確保

  • PromiseLikeのインターフェースを実装することで、ResultAsyncクラスはPromiseと互換性を持ちながらも、完全にPromiseに依存しない設計が可能になります。
  • ResultAsyncPromiseに似た振る舞いをしますが、独自のメソッド(map, andThen, mapErrなど)を持っており、Promiseクラスそのものの動作を完全に覆うことなく、必要な機能だけを提供できます。

2. 独自機能の追加

  • ResultAsyncResultの機能を組み合わせた非同期処理を扱うクラスです。これには、Resultの特性(OkErrなど)を保持しつつ、非同期処理の流れを管理するという目的があります。
  • PromiseLikeを実装することで、thenメソッドを備えつつ、通常のPromiseとは異なる振る舞いや独自のメソッドを追加できるため、ResultAsyncの特性を保ちながら非同期処理を行えます。

3. 互換性と拡張性

  • **PromiseLike**はPromiseと互換性があります。つまり、PromiseLikeを実装することで、ResultAsyncは他のPromiseと同じように使うことができますが、Promiseクラスの完全な実装をする必要はありません。
  • ResultAsyncクラスは、Promiseの一部の機能だけを持ち、必要な独自機能を追加することで、Promiseの持つ制約から自由になります。これにより、ResultAsyncクラスはより特化した非同期処理を行うための柔軟な設計が可能です。

4. TypeScriptの型システムとの親和性

  • PromiseLikeを使うことで、TypeScriptの型システムを最大限に活用しつつ、ResultAsyncのインターフェースをより軽量に保つことができます。
  • また、PromiseLikethenメソッドだけを要求するため、ResultAsyncPromiseに依存することなく、必要な部分だけを実装できます。これにより、軽量で効率的な非同期処理クラスを作ることができます。

まとめ

ResultAsyncPromiseLikeを実装する理由は、Promiseと互換性を持ちながらも、Promiseに縛られずに独自の機能を持たせるためです。これにより、ResultAsyncPromiseの利便性を享受しつつ、Resultの特性を生かした非同期処理を柔軟に行うことができます。

asyncMap

asyncMapメソッドの概要

  • 役割: asyncMapは、Result<T, E>Ok値を非同期関数で変換し、ResultAsync<U, E>を返します。
  • 動作:
    1. Okの場合: 非同期関数を使って値を変換し、新しいResultAsync<U, E>を返します。
    2. 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メソッドは、ResultAsyncOk値を取得するためのメソッドで、もしErrの場合は指定したデフォルト値を返すというものです。以下にその特徴を簡潔にまとめます。

unwrapOrメソッドの役割

  • 目的: Ok値を取得するか、もしResultAsyncErrの場合はデフォルト値を返すために使用します。

メソッドのシグネチャ

unwrapOr<A>(v: A): T | A
  • T: ResultAsyncが持つ成功時の型です。
  • A: Errの場合に返すデフォルト値の型です。
  • 戻り値: ResultAsyncOkの場合はその値(T)、Errの場合はデフォルト値(A)を返します。

動作の流れ

  1. Okのとき: ResultAsyncOkを持っている場合、そのOkの値を返します。
  2. Errのとき: ResultAsyncErrの場合、引数で指定したデフォルト値を返します。

使用例

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オブジェクトがOkErrかに応じて、対応する関数を実行し、その結果を返すためのメソッドです。このメソッドを使用することで、Resultの状態に基づいて異なる処理を行うことができます。以下にその特徴をまとめます。

matchメソッドの役割

  • 目的: ResultOkの場合と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型の値が返されます。

動作の流れ

  1. Okの場合: okコールバック関数が実行され、その結果が返されます。
  2. 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を使うことで、OkErrの両方のケースを一つのメソッドで処理できます。
  • 統一された戻り値の型: mapmapErrと異なり、matchでは両方の関数が同じ戻り値の型を持つ必要があります。これにより、戻り値の型が一致することが保証されます。
  • 柔軟な処理: matchを使うことで、Resultの内容に応じた柔軟な処理を簡潔に記述できます。

まとめ

  • matchメソッドは、Resultの状態に応じてOkまたはErrの処理を行い、その結果を返します。
  • 統一された戻り値: matchは、OkErrで異なる処理を行いながらも、同じ型の戻り値を返すことができます。
  • 使いやすさ: Resultの状態に応じた処理をシンプルに記述でき、エラーハンドリングや成功時の処理を一貫して行うことができます。

safeUnwrap

safeUnwrapメソッドは、Rustの?演算子に似た動作をエミュレートし、安全にResultOk値を取り出すためのメソッドです。もしResultErrの場合は、そのエラーをジェネレーターとして返します。以下に、その特徴をまとめます。

safeUnwrapメソッドの役割

  • 目的: ResultOkの場合はその値を取り出し、Errの場合はそのエラーをジェネレーターとして返すために使用します。これは、Rustの?演算子のように、エラーが発生した場合にその場で処理を中断し、エラーを返すことを可能にします。

メソッドのシグネチャ

safeUnwrap(): Generator<Err<never, E>, T>
  • T: Okの値の型です。
  • E: Errのエラーの型です。
  • 戻り値: Generator<Err<never, E>, T>Okの場合はTを返し、Errの場合はErr<never, E>をジェネレーターとして返します。

動作の流れ

  1. Okの場合:
    • Okの値が取り出され、ジェネレーターはその値を返します。
  2. 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メソッドは、ResultOk値を安全に取り出し、Errの場合はエラーをジェネレーターとして返すメソッドです。
  • 使いやすさ: Rustの?演算子に似たエラーハンドリングをTypeScriptでも実現できます。
  • ジェネレーターとの連携: このメソッドを使うことで、処理の中断やエラーハンドリングがジェネレーターを通じてスムーズに行えます。
補足 ジェネレーターについて

ジェネレーター関数とジェネレーター式についての概要

ジェネレーター関数とジェネレーター式は、JavaScript/TypeScriptにおける特殊な関数の一種で、関数の実行を途中で一時停止し、外部からの指示で再開することができる機能を持っています。これにより、複雑なイテレーションや非同期処理をシンプルに扱うことができます。

1. ジェネレーター関数 (function*)

  • 定義方法: 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 (すべての値を生成したので完了)
    

2. yieldキーワード

  • 役割: ジェネレーター関数内で、値を外部に返しつつ、関数の実行を一時停止します。next()が呼ばれると、前回のyieldで停止した場所から実行が再開されます。
  • 特徴:
    • 値の生成: yieldは、ジェネレーター関数が返す値を生成し、外部に渡します。
    • 制御の移譲: ジェネレーターの外部に制御を渡し、次のnext()呼び出しまで関数の実行を停止します。

3. yield*キーワード

  • 役割: 別のジェネレーター関数や反復可能オブジェクト(配列、文字列など)を展開して、そのすべての要素を順に生成します。

  • 特徴:

    • 委譲: 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));
    

ジェネレーターとプロミスの違い

  1. 目的:

    • ジェネレーターは逐次処理の流れを制御するために使われ、同期的な処理や反復処理のカスタマイズが得意です。
    • Promiseは非同期処理の完了を待つために使われ、非同期のエラーハンドリングやチェーン処理が得意です。
  2. 動作:

    • ジェネレーターは関数を一時停止し、再開することができます。
    • Promiseは非同期処理が完了したら、結果を受け取ることができます。
  3. 使用場面:

    • ジェネレーターは複雑な逐次処理、特にイテレーションやコルーチンに適しています。
    • Promiseは非同期のAPI呼び出し、タイマー、ファイル操作などに適しています。
  4. 構文:

    • ジェネレーター: function*, yield, yield*
    • Promise: new Promise, .then(), .catch(), .finally()

まとめ

  • ジェネレーターは、処理の流れを細かく制御でき、主に逐次処理やイテレーションに使われます。
  • Promiseは、非同期処理の結果を扱うためのツールで、非同期処理が完了した時に何をするかを定義できます。

両者は異なる目的で使用されますが、組み合わせることで、強力な非同期処理と逐次処理のコントロールを実現することもできます。

_unsafeUnwrap

_unsafeUnwrapメソッドは、Result<T, E>オブジェクトからOkの値を強制的に取り出すためのメソッドです。ただし、ResultErrの場合にはカスタムエラーオブジェクトをスローするため、非常に危険であり、通常はテスト環境でのみ使用されるべきものです。以下に、このメソッドの特徴をまとめます。

_unsafeUnwrapメソッドの概要

  • 役割: ResultOkである場合に値を強制的に取り出し、Errである場合は例外をスローするためのメソッドです。安全ではないため、通常のアプリケーションコードではなく、テスト環境での使用が推奨されます。

メソッドのシグネチャ

_unsafeUnwrap(config?: ErrorConfig): T
  • T: ResultOk状態に含まれる値の型です。
  • config(オプション): エラーが発生した際に、カスタムエラーオブジェクトをスローするための設定情報を含むオプションのパラメータです。

動作の流れ

  1. Okの場合:
    • ResultOkの状態であれば、その値を直接返します。
  2. Errの場合:
    • ResultErrの状態であれば、設定されたカスタムオブジェクト(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メソッドは、ResultOkであればその値を取り出し、Errであればカスタムエラーをスローする、非常に危険なメソッドです。
  • 主にテスト環境で使用: このメソッドは、通常のアプリケーションコードではなく、テスト環境でエラーが発生しないことを前提にした処理を行うために使用されます。
  • 使用にあたっての注意: Errが発生した場合に例外がスローされるため、適切なエラーハンドリングがないと予期しないクラッシュを引き起こす可能性があります。
補足unsafeとの比較

TypeScriptの_unsafeUnwrapメソッドは、Rustのunsafeブロックやunsafe関数と似た概念ですが、いくつか重要な違いがあります。ここでは、その類似点と違いを説明します。

Rustのunsafe

  • 役割: Rustのunsafeは、通常のRustの安全性保証を無効にして、危険な操作を行うことを許可するキーワードです。例えば、未定義動作を引き起こす可能性があるポインタ操作や、メモリの手動管理、外部関数呼び出し(FFI)などが該当します。

  • 使用例:

    unsafe {
        // ここでは安全性が保証されない操作が行われる
        some_unsafe_function();
    }
    
  • リスク: unsafeブロック内のコードは、通常のRustの所有権、借用、型システムによる安全性チェックの一部が無効化されるため、バグやメモリの安全性の問題が生じやすくなります。

TypeScriptの_unsafeUnwrap

  • 役割: TypeScriptの_unsafeUnwrapは、ResultErrの場合でも強制的に値を取り出そうとするメソッドです。失敗すると例外がスローされますが、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のunsafe: 高度なパフォーマンスチューニングやシステムプログラミングなど、必要不可欠な場合に限って使用されます。
    • _unsafeUnwrap: テストや特定のシナリオでエラーハンドリングをスキップしたい場合に使用されます。

まとめ

  • **_unsafeUnwrap**は、Rustのunsafeに似た名前を持つメソッドですが、実際にはメモリ管理や未定義動作のリスクがないため、意味合いが異なります。
  • 慎重な使用: 両者ともに、通常の安全なプログラミング慣習を逸脱する操作を行うため、慎重に使用する必要があります。_unsafeUnwrapは、エラーを無視して強制的に値を取得するため、予期しない例外が発生するリスクがあります。

_unSafeUnWrapErr

_unsafeUnwrapErr(config?: ErrorConfig): Eは、ResultErrの場合、そのエラー値を強制的に取り出すメソッドです。しかし、ResultOkの場合はカスタムエラーオブジェクトをスローします。このメソッドも通常のアプリケーションコードでは危険であり、主にテスト環境での使用が推奨されます。

ポイント

  • 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つのバリアント(列挙型の値)を持っています:

  1. Ok(T): 操作が成功した場合、このバリアントが選択され、T型の値を保持します。
  2. 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),
    }
}

コードの説明

  1. read_file関数:

    • 引数: filenameとしてファイル名(文字列)を受け取ります。
    • 戻り値: Result<String, io::Error>を返します。成功した場合はファイルの内容(String)を返し、失敗した場合はio::Error型のエラーを返します。
    • 処理の流れ:
      1. File::open(filename)を呼び出して、指定されたファイルを開こうとします。ファイルのオープンに成功するとfileFileオブジェクトが格納されますが、失敗するとErr(io::Error)を返します。
      2. file.read_to_string(&mut contents)を使って、ファイルの内容をcontentsというString型の変数に読み込みます。この操作も、成功すればOk(())を返し、失敗すればErr(io::Error)を返します。
      3. 最終的に、ファイルの内容をOk(contents)として返します。
  2. main関数:

    • read_file("example.txt")を呼び出して、example.txtというファイルを読み込みます。
    • match式を使って、read_fileの結果をOkErrに分岐して処理します。
      • Ok(contents)の場合: contentsにファイルの内容が格納されており、それをprintln!で出力します。
      • Err(e)の場合: ファイルの読み込みに失敗した場合で、エラー内容をeとして受け取り、そのエラーメッセージをprintln!で出力します。

サンプルコードのまとめ

このコードは、ファイルを開いてその内容を読み込む処理を行い、読み込みが成功したか失敗したかに応じて適切なメッセージを出力するというものです。Result型を使うことで、ファイル操作が成功した場合と失敗した場合の両方をしっかりとハンドリングしています。

Result型の活用方法

  • エラーハンドリングを強制する: RustではResult型を返す関数を呼び出すとき、必ずその結果を処理しなければなりません。これにより、エラーを見逃すリスクを減らすことができます。

  • match式で分岐処理: Result型の値に対してmatch式を使い、OkErrの場合で異なる処理を行うことができます。

  • ?演算子の使用: ?演算子を使うと、Result型がErrの場合に即座に関数から抜け出し、呼び出し元にエラーを返すことができます。Okの場合はその値を自動的に展開します。これにより、エラーハンドリングコードが簡潔になります。

なぜResult型を使うのか

Result型を使うことで、エラーが発生したときの対応を強制的に行わせることができ、プログラムの堅牢性が向上します。Rustは例外をスローしない言語であり、Result型を使ってすべてのエラーを明示的に扱うことで、予期せぬエラーによるクラッシュを防ぐことができます。これにより、安全で信頼性の高いコードを書くことが可能になります。

Rustとneverthrowの違い

neverthrowResultはクラスとして定義されていますが、RustのResultは型として定義されています。この違いは、各言語の特性と設計思想に基づいていますが、基本的な役割や機能は似ています。

  • RustのResult: Rustでは、Resultは型(列挙型)であり、型システムの一部として動作します。これは、コンパイラによって厳密にチェックされる静的な型システムを活用して、安全なエラーハンドリングを実現しています。

  • neverthrowResult: neverthrowでは、Resultはクラスとして定義されており、JavaScript/TypeScriptの柔軟な環境に合わせて設計されています。TypeScriptの型システムを使って、RustのResultに似たエラーハンドリングを可能にしています。

どちらもエラーハンドリングを強化するための仕組みですが、それぞれの言語の特性に応じて設計されているという点で異なります。

Discussion