JavaScriptのawait書き忘れの防ぎ方を調べた

こんにちは、最近業務とテニスを盾に更新が滞ってました。ダブルシールドです。え、片方がシールドになってない???そういえば両手に剣を持ってる戦士はたまにいますが、両手に盾持ってる戦士はあまりいませんね。
さて、今日はJava Script(node)を書いてるとやらかしがちなawait書き忘れの防ぎ方について書きます。await書き忘れ問題防ぎたいけど、TypeScriptよく知らないけど、でも手っ取り早く(そしてざっくり)内容掴みたいというニッチな人向けのピンポイント記事。と言うか自分用メモ。

1. await書き忘れ問題

await、書き忘れると厄介ですね。ランタイムエラーが出ない割に、予想できない振る舞いをするわ、見つけにくいわで大変です。詳しくはこちらの方が書かれてます。「デフォルトはawaitにして、noawait的なキーワードを作った方がいいでしょ」って思いますね。1残念ながら、当面実装されなさそうですが async/await: nowait keyword?

2. await 書き忘れ問題の防ぎ方3つ

さて、答えは簡単、静的解析的なアプローチです。具体的には、次の3つ

  1. @typescript-eslint/no-floating-promises
  2. @typescript-eslint/no-misused-promises
  3. そこそこ正しい型づけを使ったTypeScriptによるコンパイル

ESLint(静的解析ツール)をまだやってない方は、この際ESLintも始めましょう(丸投げ)。もしQiita記事を1つ選ぶなら、私はこちらが好みです。
それから、安心してください、1と2はTypeScript(Java Scriptに型がついた言語、たぶん)へ移行しなくても効果が得られます。2今はまだ、、、たぶん今はまだその時やないんや。。。!それぞれざっくり説明します。3余談。もともとTSLintというTypeScript向けの静的解析ツールがあり、1と2はTSLintのルールでした。しかし、TSLintはDeprecatedになり、ESLintへの統合が進んでます。TypeScript向けに作られてるので、TypeScript使ってる前提でしか動かないように思いますが、普通にチェックかけれました。TypeScriptには、Type Checking JavaScript Filesなる機能があり、おそらくそれで型推論されてる?よくわからんがまぁ動いてるから、ヨシ!

1. @typescript-eslint/no-floating-promises

@typescript-eslint/eslint-plugin というESLintのプラグインがあるのですが、これはその静的解析ルールです。これを有効化して静的解析(e.g. $ npx eslint ...)すると、こういう感じのパターンを検知します。

async function bar() {
  // なんかこう、asyncなことをする
}

async function foo() {
  bar(); // @typescript-eslint/no-floating-promises Error
}

名前の如く、宙に浮いたpromiseを禁止するルールですね。詳しくはこちら

余談: eslintの警告を無視したい

  1. 意図的にawaitをつけたくない場合は、対象行の先頭にvoidとつけるとESLintの警告が出なくなります
  2. あるいは、特定行を無視するESLint用のコメントを書くでも良いかと思います。

2. @typescript-eslint/no-misused-promises

これも同じく@typescript-eslint/eslint-plugin の静的解析ルールです。これを有効化して静的解析(e.g. $ npx eslint ...)すると、こういう感じのパターンを検知します。

async function bar(){
  // なんかこう、asyncなことをする
  return false;
}

async function foo() {
  if (bar()) {} // @typescript-eslint/no-misused-promises Error
}

詳しくはこちら。自分で書いといてあれですが、こういう使われ方するんですね。

3. TypeScriptのそこそこ正しい型づけ

4^ 筆者のTypeScriptに対する自信の度合いに関する機敏を表現しております TypeScriptで、関数や変数の型づけが必要でコンパイル(e.g. $ npx tsc ...)しないとわからないパターンです。具体的には次のとおりです。

async function bar(): Promise<void> {
  // なんかこう、asyncなことをする
  return false;
}

async function foo() {
  const result: boolean = bar(); // .ts -> .jsにコンパイルした時に次のようなエラーがでる
  // Type 'Promise<void>' is not assignable to type 'boolean'.
}

1および2と異なり、TypeScriptの(雑な)型づけを行ってます。

ただし、Java Scriptで同等のことを実装してしまっても、気付く可能性はわりと高いです。(resultにPromise的な変数が入ってくるので、後段の処理で多分気付く)。なので、わざわざESLintとしても静的解析ルールは必要ないのかなと。

余談1 console.log

console.log の引数型はAny(どんな型でもブッ込める、TypeScriptを実質的に無効化する型)。そのため、これにPromiseの結果を直接ブッ込むとおそらくawaitの書き忘れを検知できない。

余談2 tscコマンドの使い方

  1. tscコマンドは ファイル指定せずに実施する方が良い。さもないとこう言う感じの謎エラーに見舞われる
src/main.ts:14:3 - error TS2585: 'Promise' only refers to a type, but is being used as a value here. Do you need to change your target library? Try changing the `lib` compiler option to es2015 or later.

14   Promise.all([f(), p]);       // no-floating-promises Error because of .all
     ~~~~~~~

3. その他、TypeScriptの細かいメモ走り書き

クラスの文法

  1. TypeScript的な(?)クラスの書き方がある模様。これに従うと await書き忘れを検知できるはず
  2. 最近触ってるJava Scriptのクラスはこちらの記事の感じで作ってます。(そもそもJava Scriptはプロトタイプベースの言語なので、オブジェクト指向的な文法はもともとなかった)

import/exportの文法

  1. モジュール間でawait書き忘れを検知するにあたり、正しく(?)importをする必要がある
    1. https://www.typescriptlang.org/docs/handbook/modules.html#testts-2

外部モジュールのawait忘れ検知

  1. おそらくこの辺のmoduleを使って型づけする必要がある(あまりちゃんと調べられてない)
    1. https://github.com/DefinitelyTyped/DefinitelyTyped (おそらく中央管理されてるやつ)
    2. https://www.npmjs.com/package/@types/request-promise (その一例)

4. まとめ

猛ダッシュした割に長くなりましたが以上です。もともとawait書き忘れを防ぎたい一心でいろいろ調べた結果、TypeScriptをよく知らないばかりに(今もそんなにわかってない)やたら苦労したのでまとめることにしました。5余談ですが、@typescript-eslint/no-misused-promises ルールが思ってたとおり動かなくて、わからなさすぎて、typescript-eslintのコミュニティに質問投げたりもしました。聞いた場所が正しかったか今もよくわかってないんですが、わざわざコードまで見てくれて、めっちゃアドバイスくれました。最高かよ
ちなみに、「3. TypeScriptのそこそこ正しい型づけ」に関して、TypeScript必要と述べましたが、実はいらないかもです。TypeScriptにはJSDocから型を特定する機能があるので、JSDocを書けばうまくいくかも。試してないので、知ってる方教えてくだされ

5. 参考

  1. async/await: nowait keyword?
  2. Disallow calls to async functions from async functions without using ‘await’

6. おもちかえり

  1. https://github.com/mwakizaka/detect_missing_await
  2. await書き忘れがどの程度防げそうか確認できます
  3. .eslintrc.jsやtsconfig.jsonはわりと雑に作ったので、ほどほどに参考にしてくだしあ