NeverThrowを触ろうと思ったらESLintのFlat Configを触っていた(前編)

TypeScriptでサーバーサイドのアプリケーションを書いていて、エラーをThrowするより型安全に失敗を返したいなと思って、簡単なResult型をペラっと作って使ったことがある。

あまり複雑なことはしたくないからそれで満足しているんだけど、TSKaigiやTSKaigi KansaiでResult型の話をよく耳にしたので、ちゃんとライブラリを一個くらい勉強しようかなと思った。

最初にあらすじ

  • neverthrowとeslint-plugin-neverthrowを入れてNeverThrowを触ろうとしたら、eslint-plugin-neverthrowがESLintのFlat Configに対応していなかった
  • Flat Configを調べるところから始めて、最終的にeslint-plugin-neverthrowの問題はESLintが提供しているラッパーで解決した
  • やったー!NeverThrowで遊べるぞー!←いまここ(つまりまだ遊んでない)

長くなりそうなので途中で切った。

NeverThrowを触ってみることにした

Result型が使えるライブラリってどういうのがあるの?って同僚の @kosui_me に聞いたら「fp-ts・Effect・NeverThrowがあるよ」って教えてもらったのと、@sadnessOjisan の記事を見かけてoption-tを知った。

ほんとにちらっとだけ見てみて、fp-tsとEffectはResult型よりも広い範囲をカバーしてそうだなって感じた。関数型のプログラミングや、実現したいエコシステムがあって、その一つの要素としてResult型があるような印象。それはちょっと僕には大きいので、今回はやめておこうと思った。

で、NeverThrowとoption-tはResult型をメインで扱っているのでどちらもいいなーと思いつつ、GitHubを眺めたりしてなんとなくNeverThrowを触ってみることにした。

↓ @sadnessOjisanの記事。とてもいい。

NeverThrowを試すための環境をセットアップ

NeverThrowを試すための簡単な環境をセットアップする。

↑に、こう書いてある。

> npm install neverthrow

> npm install eslint-plugin-neverthrow

なるほどね処理し忘れを教えてくれるESLintもあるのね。便利そう。じゃ、TypeScriptとESLintをセットアップしてNeverThrowで遊べばよさそう。

TypeScriptのプロジェクトを作る

じゃ、まずTypeScriptのプロジェクトを作る。

# プロジェクトフォルダを作って
❯ mkdir hello-result
❯ cd hello-result

# package.jsonを作って
❯ cat <<EOL > package.json
{
  "name": "hello-result",
  "type": "module"
}
EOL

# typescriptを追加して
❯ pnpm add -D typescript
❯ pnpm tsc --version
Version 5.7.2

# tsconfigを作る
❯ cat <<EOL > tsconfig.json
{
  "compilerOptions": {
    "target": "ESNext",
    "module": "NodeNext",
    "esModuleInterop": true,
    "forceConsistentCasingInFileNames": true,
    "strict": true,
    "skipLibCheck": true,
  }
}
EOL

tsxを入れておこうかな

適当に実行できるように tsx を入れておく

❯ pnpm add -D tsx

❯ pnpm tsx --version
tsx v4.19.2
node v22.11.0

# 試してみる
❯ mkdir src
❯ cat <<EOL > src/hello.ts
console.log("hello");
EOL

❯ pnpm tsx src/hello.ts
hello

ESLintを使えるようにする

ここに書いてあるコマンドを実行した。

❯ pnpm create @eslint/config@latest
✔ How would you like to use ESLint? · problems
✔ What type of modules does your project use? · esm
✔ Which framework does your project use? · none
✔ Does your project use TypeScript? · typescript
✔ Where does your code run? · node
The config that you've selected requires the following dependencies:

eslint, globals, @eslint/js, typescript-eslint
✔ Would you like to install them now? · No / Yes
✔ Which package manager do you want to use? · pnpm

そうするとこういうファイルが作られた。おー!僕が知ってる設定ファイルと違うー!これがFlat Configというやつかー!

import globals from "globals";
import pluginJs from "@eslint/js";
import tseslint from "typescript-eslint";


/** @type {import('eslint').Linter.Config[]} */
export default [
  {files: ["**/*.{js,mjs,cjs,ts}"]},
  {languageOptions: { globals: globals.node }},
  pluginJs.configs.recommended,
  ...tseslint.configs.recommended,
];

とりあえず動かす

なんか警告が出た。あぁ。そうか。まだ @typescript-eslint は5.7をサポートしてないよってことか。

❯ pnpm eslint
=============

WARNING: You are currently running a version of TypeScript which is not officially supported by @typescript-eslint/typescript-estree.

You may find that it works just fine, or you may not.

SUPPORTED TYPESCRIPT VERSIONS: >=4.7.4 <5.7.0

YOUR TYPESCRIPT VERSION: 5.7.2

Please only submit bug reports when using the officially supported version.

=============

んー。まぁ大丈夫やろ。いったんこの警告をオフにしよう。 parserOptions を足した。

...

export default [
  {files: ["**/*.{js,mjs,cjs,ts}"]},
  {languageOptions: { globals: globals.node }},
  pluginJs.configs.recommended,
  ...tseslint.configs.recommended,
  {
    languageOptions: {
      parserOptions: {
        warnOnUnsupportedTypeScriptVersion: false,
      }
    }
  },
];

(注:記事の流れ的にここに書いたけど、実際はしばらく放置してて、Flat Configのことを少し理解してから「オプションをここに書けば良さそう」ってなって警告をオフにした。まだこの時点ではFlat Configまったく理解していなかった。)

❯ pnpm eslint

よし。警告がでなくなった。じゃ、ESLintに怒られる(正しく動く)ことも確認しておくか。

let a = 1
console.log(a);
❯ pnpm eslint src/hello-error.ts

(Redacted)/hello-result/src/hello-error.ts
  1:5  error  'a' is never reassigned. Use 'const' instead  prefer-const

✖ 1 problem (1 error, 0 warnings)
  1 error and 0 warnings potentially fixable with the `--fix` option.

OK。ちゃんと怒ってくれた。

NeverThrowを追加

NeverThrowを追加して、動作確認しておこう。

❯ pnpm add neverthrow

こんな感じにしてみた。

import {err, ok, Result} from "neverthrow";

const hello = (flag: boolean): Result<string, Error> => {
  if (flag) {
    return ok("hello");
  }
  return err(new Error("failure"));
}

const result = hello(true);
console.log(result);

実行したら↓こうなる。よし。動いてる。

❯ pnpm tsx src/hello-neverthrow.ts
Ok { value: 'hello' }

NeverThrowのLinterを追加

じゃ、最後にLinterを追加して準備オッケーだな。

❯ pnpm add eslint-plugin-neverthrow
Packages: +7
+++++++
Progress: resolved 155, reused 131, downloaded 0, added 7, done

dependencies:
+ eslint-plugin-neverthrow 1.1.4

Done in 4s

2024-11-24時点で最新の eslint-plugin-neverthrow 1.1.4 が入った。

設定は・・・っと

ここを見てみる

module.exports = {
  plugins: ['neverthrow'],
  rules: {
    'neverthrow/must-use-result': 'error',
  },
  parser: '@typescript-eslint/parser',
  parserOptions: {
    ecmaVersion: 2021,
    sourceType: 'module',
    project: ['./tsconfig.json'],
    tsconfigRootDir: __dirname,
  },
};

あー。Flat Configじゃないから・・・どうすればいいんだっけ・・・。

選択肢は

仕事だったらとりあえず①を選んでおいて自由時間で調べそう。今日は遊びなので③でしょ。Flat Configで eslint-plugin-neverthrow を使えるようにするぞー!

つづく

ここまでのコードをpushして、続きはまた後で書く。今日は焼き芋を焼くー。

つづく

追記:中編書いた↓