Closed35

TSのResult型について知りたい

hajimismhajimism

この記事がきっかけで、そもそものResult型のMotivationから整理するのをやりたくなった。自分も各地でおれおれResultを見てきた経験があって...
https://zenn.dev/chot/articles/0993e96e8705d3

hajimismhajimism

いま中途半端に色々読んじゃってて理解の途中からスクラップを開いた形になる。スクラップ自体は途中からだけど、スクラップ開ける前に読んだ記事もあとで貼る。

hajimismhajimism

https://saneyukis.hatenablog.com/entry/2023/02/01/030251

fp-tsとneverthrowとoption-tがどうやら代表的なライブラリらしいというのがなんとなくわかっていて、Ruselt型だけ導入したいときは後ろ2つのどちらかを選ぶことが多くて、その2つの比較をoption-tの作者が書いてくれているらしい?

hajimismhajimism

neverthrowと比較した場合の設計意図の違いについて

ざっと見た場合、以下が設計方針として違うかなと思う。

  • documentation(上で述べた通り)
  • メソッドチェーンの有無
    • classベースの実装か否か
    • tree shakingのサポート
  • Result 型以外の実装も提供している
hajimismhajimism

result-type-tsではTree shakingを取らなかったらしいが
https://zenn.dev/chot/articles/0993e96e8705d3

こういう設計にするとTree shakingが効かなくなってしまうのですが、Result型だけの小さなライブラリなので普通のプロジェクトではバンドルサイズはほとんど誤差になってメリットの方が上回るのではないかと考えました。

option-tではバンドルサイズを重視してこの選択

一般的なアプリケーションから、配布サイズにセンシティブなSDKや配信ウィジェットでの利用まで含めて実用的にしようと思うと、この選択となった。

Result 型に対するオペレータ関数は実質無限に近しいパターンを実装可能であり、頻出パターンはライブラリとして提供するのが望ましい一方、それら全てが常に使われるわけでは無いという問題と隣り合わせである。

どっちもわかる、実際どのくらいの差が出るんだろう。

hajimismhajimism
hajimismhajimism

And Rust's std::option and std::result are suggestive to achieve these conventions in practice. Thus this package is inspired by their design.

そう、なんか局所的にRustのドキュメント読めばわかるよっていうのを何回か見かけた。

hajimismhajimism

Uniform the expression of "none" value.

「undefined / null / -1 みたいに色々あるけど統一したい」

hajimismhajimism

Uniform the way to carry error information instead of throwing an error.

Thus this library provides a way to express recoverable error and also recommends to use throwing an error only if you intend to throw an unrecoverable error. This categorization introduces a convenient convention for you:

  • If the code uses throw, you should be careful about unrecoverable error.
  • If the code returns Result<T, E> provided this library, then you should handle it correctly.

Rustの「回復可能かどうか」というエラー分類にinspireされ、「回復可能ならResult型を返し、そうじゃないならthrow Errorする」という考え方をしているらしい

hajimismhajimism

https://zenn.dev/chot/articles/0993e96e8705d3

には

なので既に何人もの開発者がResult型のnpmパッケージを公開しているのですが、自分好みのものが見当たらなかったので自作しました。

とあるが、どこらへんが「自分好み」(=他との差別化)なんだろう

hajimismhajimism

そのおかげでResultだけをimportすれば済みますし、関数名などを覚えていなくてもエディターの候補表示から全てのユーティリティを辿れるようになっています。

これかな?

hajimismhajimism

あーいや、interfaceだいぶ違うな?

neverthrowは.unwrapOrで値を取り出すけど、こっちは.valueで取り出す?

hajimismhajimism

いや、ありそう。READMEのexampleより・

import { findUsersIn } from 'imaginary-database'
// ^ assume findUsersIn has the following signature:
// findUsersIn(country: string): ResultAsync<Array<User>, Error>

// Let's say we need to low-level errors from findUsersIn to be more readable
const usersInCanada = findUsersIn("Canada").mapErr((error: Error) => {
  // The only error we want to pass to the user is "Unknown country"
  if(error.message === "Unknown country"){
    return error.message
  }
  // All other errors will be labelled as a system error
  return "System error, please contact an administrator."
})

// usersInCanada is of type ResultAsync<Array<User>, string>

usersInCanada.then((usersResult: Result<Array<User>, string>) => {
  if(usersResult.isErr()){
    res.status(400).json({
      error: usersResult.error
    })
  }
  else{
    res.status(200).json({
      users: usersResult.value
    })
  }
})
hajimismhajimism

どこらへんが「自分好み」(=他との差別化)なんだろう

まだResult型をちゃんと使ったことがないのでよくわからなかった。

今のところの印象ではメソッドチェーンを書きたいのでoption-tよりはneverthrowのが好きそう。ただ、richすぎるのはそうだなって思う。全部のメソッドを使いこなす気がしないのでtree shakingしてくれはそうかも。

hajimismhajimism

neverthrow使ってみて「べつにメソッドチェーンそんな使わんわ」ってなったらoption-tすればいいのか

hajimismhajimism

逆かな?option-tから始めて、あまりに書きにくかったらneverthrowで改善できるかどうかを考えればいいのか

hajimismhajimism

個別の書き味とかに深入りする前にもうちょっと概観したくて

hajimismhajimism

いろんなところで言及されてるのがこの記事
https://blog.ojisan.io/my-new-error/

hajimismhajimism

Result型の主なモチベーションは理解した

しかし f1 が例外を投げるかどうかは実装を読まないとわからないので、try catch を使うのを忘れることがあるかもしれない。特にライブラリを使っていると、そのライブラリの挙動を完全に知っていないといつ例外が投げられるかという恐怖がつきまとい、難しい問題だ。
(中略)
として val を使うためには必ず f1Result の ok, error 検証が必要となる。 つまり失敗するかもしれないという文脈を型検査で確かめることが強制されるのである。

hajimismhajimism

この記事でoption-tが推されてて、それを読んだ人がneverthrowが好きって言ってて、それを見かけたoption-tの作者が比較記事を書いてくれたのだった

hajimismhajimism

あ、Rustのドキュメント読めばわかるよってここに書いてあったんだ。

このライブラリのいいところは Rust の標準ライブラリに影響を受けているので、Rust 標準ライブラリの combinator が備わっていたり、使い方のドキュメントは Rust のドキュメントを読めばいいところにある。Rust は難しい印象もあるがドキュメントの生成機能がすごいこともあって Example の充実がすごく、Rust を読めなくても Result のリファレンス・教科書としてまで使えるクオリティなのでチームに導入する時も使いやすい。

hajimismhajimism

fetchの例がわかりやすい。fetchて大変だな...

  async getUserById(id: number): Promise<Result<Object, RepositoryError>> {
    let res;
    try {
      res = await fetch(`${URL}/users/${id}`);
    } catch (e) {
      const error = new FetchMethodError(
        JSON.stringify({
          reason: "fail to fetch",
          url: URL,
          payload: { id },
        }),
        { cause: e }
      );
      loggingException(error);
      return createErr(error);
    }

    if (!res.ok) {
      switch (res.status) {
        case 401: {
          const error = new AuthorizationError(
            JSON.stringify({
              reason: "fail to fetch by miisng auth",
              url: URL,
              payload: { id },
              res,
            })
          );
          loggingException(error);
          return createErr(error);
        }
        default: {
          const error = new InternalError(
            JSON.stringify({
              reason: "internal server error",
              url: URL,
              payload: { id },
              res,
            })
          );
          loggingException(error);
          return createErr(error);
        }
      }
    }

    let data;
    try {
      data = await res.json();
    } catch (e) {
      const error = new ResponseParseError(
        JSON.stringify({
          reason: "fail to parse",
          url: URL,
          payload: { id },
        }),
        { cause: e }
      );
      loggingException(error);
      return createErr(error);
    }

    if (validate(data)) {
      return createOk(data);
    } else {
      const error = new ValidationError(
        JSON.stringify({
          reason: "fail to validate",
          url: URL,
          response: { data },
        })
      );
      loggingException(error);
      return createErr(error);
    }
  }
hajimismhajimism

Typeboxをつかってる手元のプロジェクトでは、こういうlib書いておけばいいのでは、となった

export const callApi = async <T extends TSchema>(
  path: string,
  validator: TypeCheck<T>,
  init?: RequestInit
): Promise<Result<Static<T>, RepositoryError>> => {
  let res
  try {
    res = await fetcher(path, init)
  } catch (e) {
    const error = new FetchMethodError(
      JSON.stringify({
        reason: 'failed to fetch',
        path,
      }),
      { cause: e }
    )
    return fail(error)
  }

  if (!res.ok) {
    switch (res.status) {
      case 401: {
        const error = new AuthorizationError(
          JSON.stringify({
            reason: 'failed to fetch by miisng auth',
            path,
            res,
          })
        )
        return fail(error)
      }
      default: {
        const error = new InternalError(
          JSON.stringify({
            reason: 'internal server error',
            path,
            res,
          })
        )
        return fail(error)
      }
    }
  }

  let data
  try {
    data = await res.json()
  } catch (e) {
    const error = new ResponseParseError(
      JSON.stringify({
        reason: 'failed to parse',
        path,
      }),
      { cause: e }
    )
    return fail(error)
  }

  if (!validator.Check(data)) {
    const cause = Array.from(getJobsResponseValidator.Errors(data))
    const error = new ValidationError(
      JSON.stringify({
        reason: 'failed to validate',
        path,
        response: { data },
      }),
      { cause }
    )
    return fail(error)
  }

  return succeed(data)
}

hajimismhajimism

この記事で、↑の記事と別角度の実践テクが紹介されてた
https://zenn.dev/yoshiko/articles/7ff389c5fe8f06

hajimismhajimism

全体設計からみたときの話と、「Result使ってても返り値に着目しないときに処理忘れがちよね?」みたいな話が目新しかったかな?

const handleDelete = async (id: string) => {
  await deleteUser(id)
}
hajimismhajimism

option-tは結果が T | null | undefinedになるのいやかなって思ったんだけど、Tに絞らせる術はあるだろうか

ふつうに早期リターンで絞れた

このスクラップは2023/09/04にクローズされました