6
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

【ESLint 9 対応】Typed Linting で非同期処理の恐怖と戦おう【Vue + TypeScript】

Last updated at Posted at 2025-09-25

はじめに

本記事では、 ESLint 9 × Vue.js × TypeScript 環境で Typed Linting を導入する方法と、検出できる不具合の事例、導入の際の注意点、今後の展望をご紹介します。

この記事は 2025/9/8 時点の情報に基づいています。

まとめ

本記事では以下を紹介します。

  • Typed Linting の概要
  • no-floating-promises などの非同期処理の不具合検出に有効なルールの活用例
  • ESLint 9 × Vue.js × TypeScript 環境での設定方法
  • vue-router や Top level await のように、例外対応が必要なケース
  • 導入時のメリット(不具合の早期発見)とデメリット(Linting の遅さ)
  • ツールの発展によるデメリット解消の展望

Typed Linting は一見地味な改善ですが、非同期処理にまつわるバグは検出が難しく、後工程でのトラブルにつながりやすいものです。 Lint 段階で検知できるようになることは、開発効率や品質向上に直結します。

現時点では大規模プロジェクトにおいてパフォーマンス面での懸念はありますが、 ESLint が oxlint (Rust 製のリンター)に、 tsc が tsgo (Go 製の TypeScript コンパイラー) へ進化することで、将来的に解決が見込まれます。

プロジェクトを新規に立ち上げる場合はもちろん、既存プロジェクトでも導入を検討する価値は十分にあるでしょう。

パッケージのバージョンとアプリケーション構成

本記事の内容に関連するパッケージについて、動作確認を行った際のバージョンを下記に示します。実際に ESLint 9 へのアップグレードと Typed Linting を導入した対象のアプリケーションは、 AlesInfiny Maia/Maris1 で公開している、 Vue.js + TypeScript で構成されたフロントエンドアプリケーションですが、本記事では、 Typed Linting の導入にフォーカスするため、 Vue.js 公式のスキャフォールディングツールである create-vue で作成したサンプルアプリケーションをベースに説明します。

パッケージ名 バージョン
eslint 9.34.0
@vue/eslint-config-typescript 14.6.0
eslint-plugin-vue 10.3.0
vue 3.5.21
typescript 5.8.3
create-vue 3.18.0

背景

ESLint 8 の EOL ( flat config 対応)

2024 年 10 月をもって ESLint 8 のサポートが終了 (EOL) し、次世代となる ESLint 9 では新しい設定方式である flat config への移行が本格的に求められるようになりました。 従来の .eslintrc ファイルを使用した設定は ESLint 10 で廃止が予定されている2ので、今後は flat config を前提にプロジェクトを構築することが標準となっていきます。

AlesInfiny Maia/Maris でも、 フロントエンドアプリケーションの静的解析に関する設定を全面的に見直し、特に ESLint については採用するルールセットを一新することになりました。この機会に導入した解析手法が Typed Linting です。

Typed Linting とは何か

より説明的な表現では Linting with type information であり、具体的には TypeScript の型情報を使用した Linting を行うことです。 type-aware linting という呼び方もあるようですが、日本語の定訳はないようですので、本記事ではこの機能を紹介している typescript-eslint のブログ のタイトルに使用されている Typed Linting と呼ぶことにします。

Typed Linting で何ができるのか

Typed Linting を有効化することで利用できる有益なルールが、no-floating-promisesno-misused-promises です。これらのルールは、適切に処理されていないPromise型を検出できるので、非同期処理に起因する不具合を静的解析の段階で抽出できます。

検出できる不具合の例

たとえば、 AlesInfiny Maia/Maris では手動テスト時に次のような不具合を検出したことがありました。

user-service.ts
-  if (!authenticationService.isAuthenticated()) {
+  if (!(await authenticationService.isAuthenticated())) {
     return
   }
   await userStore.fetchUserResponse()

本来は未認証のときは何もせず、認証済みのときはユーザーの情報を取得するという挙動を期待した実装でしたが、実際にはいつでもユーザーの情報を取得する処理を実行し、未認証状態のユーザー情報を取得しようとして 401 UnAuthorize エラーを出力してしまっていました。

なぜなら下記のようにisAuthenticated()Promiseを返却する非同期処理であったにもかかわらず、呼び出し側で await を忘れていたので、 if 文の判定が機能していなかったからです。

authentication-service.ts
  async isAuthenticated(): Promise<boolean> {
    const result = msalInstance.getActiveAccount() !== null
    const authenticationStore = useAuthenticationStore()
    authenticationStore.updateAuthenticated(result)
    return result
  },

この不具合は TypeScript の型チェックでは検出できないので CI パイプラインを通過してしまい、結果として手動テストで、開発者コンソールに HTTP エラーが出力されるまで気付くことができませんでした。

しかし、 Typed Linting を使用すると、 await されていない Promise の存在を検知して警告してくれるので、 CI の段階で不具合に気付くことができます。

経験的に、どのような言語でも非同期処理に起因する不具合は発生しやすく、かつ原因の特定に時間を要すると思われるので、これらのルールが利用可能になることはプロジェクトにとって有益であると考えられます。

Vue + TypeScript 環境での導入手順

それでは、 ESLint 9 環境で、 Vue + TypeScript のアプリケーションに Typed Linting を導入する方法を説明します。
下記のように、 create-vue を使ってプロジェクトを初期化すると、 ESLint の設定ファイルが生成されます3。もちろんこのままでも ESLint の実行が可能です。

npx create-vue@3.18.0
(中略)
◆  Select features to include in your project: (↑/↓ to navigate, space to select, a to toggle all, enter to confirm)
│  ◼ TypeScript
(中略)
│  ◼ ESLint (error prevention)
eslint.config.ts
import { globalIgnores } from 'eslint/config'
import { defineConfigWithVueTs, vueTsConfigs } from '@vue/eslint-config-typescript'
import pluginVue from 'eslint-plugin-vue'
import pluginVitest from '@vitest/eslint-plugin'
import pluginCypress from 'eslint-plugin-cypress'
import skipFormatting from '@vue/eslint-config-prettier/skip-formatting'

export default defineConfigWithVueTs(
  {
    name: 'app/files-to-lint',
    files: ['**/*.{ts,mts,tsx,vue}'],
  },

  globalIgnores(['**/dist/**', '**/dist-ssr/**', '**/coverage/**']),

  pluginVue.configs['flat/essential'],
  vueTsConfigs.recommended,
  
  {
    ...pluginVitest.configs.recommended,
    files: ['src/**/__tests__/*'],
  },
  
  {
    ...pluginCypress.configs.recommended,
    files: [
      'cypress/e2e/**/*.{cy,spec}.{js,ts,jsx,tsx}',
      'cypress/support/**/*.{js,ts,jsx,tsx}'
    ],
  },
  skipFormatting,
)

Typed Linting を導入する場合、本来はいくつか設定が必要なところを @vue/eslint-config-typescript がいい感じに設定してくれているおかげで、ルールセットを recommendedTypeChecked4 に変更するだけで動作するようになります5

- vueTsConfigs.recommended,
+ vueTsConfigs.recommendedTypeChecked,

Config Inspector で秘密を覗く

これだけではおもしろくないので、@vue/eslint-config-typescript が何をしてくれているのか確認してみましょう。
ESLint 9 では、 ESLint の設定を可視化する ESLint Config Inspector が提供されているので、これを使って設定を確認してみます。
下記のコマンドを実行すると、ブラウザー上で設定内容を確認できます。

npx eslint --inspect-config
ℹ Starting ESLint config inspector at http://localhost:7777 

image.png

いくつかポイントとなる設定をピックアップして内容を見ていきます。

typescript-eslint/recommended-type-checked

先程設定した recommendedTypeChecked の実体となるルールセットです。

typescript-eslint/disable-type-checked

javascript ファイルおよび script ブロックを持たない vue ファイルに対して、 型情報を必要とする Lint ルールを無効化しています。ここで無効化しておくことで、型情報のないファイルに対して不要なルールが適用されてエラーが発生することを防いでいます。

@vue/typescript/type-aware-rules-in-conflict-with-vue

recommendedTypeChecked の設定のうち、 Vue.js ではうまく動作しないルールを無効化しています6

@vue/typescript/default-project-service-for-ts-files

TypeScript の設定ファイル tsconfig.json を検出するために parserOptions.projectService を設定7し、 Vue ファイルの script ブロックの中の TypeScript を解析するためにparservue-eslint-parser を設定をしています。

サンプルコードでの動作確認

それでは、 Vue ファイルに対して実際に Linting を実行し、 no-floating-promises が不具合を検出するところを確認してみます。
App.vue に API 呼び出しをする非同期関数を定義して、わざと await せずに実行するような処理を実装します。

App.vue
<script setup lang="ts">
+async function fetchSample() {
+  const response = await +fetch('https://api.github.com/repos/AlesInfiny/maia')
+  return response
+}
+fetchSample()
</script>

下記のコマンドで lint を実行すると、 no-floating-promise が 6 行目の fetchSample() に対する await 忘れを検知してくれます。

npm run lint
C:\sample\app\src\App.vue
  6:1  error  Promises must be awaited, end with a call to .catch, end with a call to .then with a rejection handler or be explicitly marked as ignored with the `void` operator  @typescript-eslint/no-floating-promises

✖ 1 problem (1 error, 0 warnings)

TIPS

次のセクションでは、 AlesInfiny Maia/Maris での導入時にハマった落とし穴とその対処方法を 2 つ紹介します。

vue-router の非同期ナビゲーション処理

vue-routerを用いた定番のナビゲーション処理を実装してみます。

App.vue
<script setup lang="ts">
+import { RouterLink, RouterView, useRouter } from 'vue-router'
+const router = useRouter()
+router.push('/')
</script>

しかし、残念ながら no-floating-promise が 4 行目の push() に対して警告を出力します。

C:\sample\app\src\App.vue
  4:1  error  Promises must be awaited, end with a call to .catch, end with a call to .then with a rejection handler or be explicitly marked as ignored with the `void` operator  @typescript-eslint/no-floating-promises

✖ 1 problem (1 error, 0 warnings)

というのも実は、普段意識されていないだけで、 push()Promiseを返却する非同期処理だからです。
push() は投げっぱなしでも特に問題ない処理なので、毎回 void router.push()と書くことで警告を回避できますが、典型的なパターンに対して都度対応するには煩わしいです。

そのために例外登録ができるオプション allowForKnownSafeCalls が用意されています。
Promiseawait が不要なメソッドはホワイトリストに登録しておくことで、冗長なコーディングが不要になります。

eslint.config.ts
  {
    name: 'app/additional-rules',
    files: ['**/*.{ts,mts,tsx,vue}'],
    rules: {
      '@typescript-eslint/no-floating-promises': [
        'error',
        {
          // 戻り値の Promise を await 不要とみなすメソッドを例外登録します。
          allowForKnownSafeCalls: [
            { from: 'package', name: ['push', 'replace'], package: 'vue-router' },
          ],
        },
      ],
    },
  },

Top level await と IIFE の扱い

AlesInfiny Maia / Maris のサンプルアプリで検出された不具合として、次のようなものがありました。
initialize()が非同期処理なのにawaitしていないので、初期化処理が完了する前に次のコードが実行されてしまう可能性があるという不具合です。

authentication-service.ts
import { msalInstance } from '@/services/authentication/authentication-config'
-msalInstance.initialize()
+await msalInstance.initialize()

上記のように修正すれば解決と思いきや、意外と罠がありました。
この await ですが、よく見るとasyncメソッドの中で使われておらず、単独で使われています。
具体的には ES2022 で追加された Top Level Await という機能を使用しています。

たとえば Vite の build.target を最も広範なブラウザバージョンに対応するES2015 へ設定してビルドしてみると、 Top level await 構文が利用できない旨のエラーが出力されます。

✗ Build failed in 2.85s
error during build:
[vite:esbuild-transpile] Transform failed with 1 error:
assets/index-!~{001}~.js:23107:0: ERROR: Top level await is not available in the configured target environment ("es2015" + 2 overrides) 
file: assets/index-!~{001}~.js:23107:0

Top level await is not available in the configured target environment ("es2015" + 2 overrides)
23105|  });
23106|
23107|  await msalInstance.initialize();
   |  ^
23108|  const authenticationService = {
23109|    async signInAzureADB2C() {

よりモダンなブラウザーバージョンのみをアプリのサポート範囲とするのであれば特に問題ないのではと思われるかもしれません。
しかし、ブラウザーの互換性 を確認すると、広く使われている Safari および Safari on iOS では部分サポートとなっており、使用を避けておくほうが安全です。

その際の対処方法として、歴史的には逆行する形ですが、IIFE(即時実行関数式) 8という、 Top level await が登場する前に代替として使われていた構文を用いて同じ挙動が実現できます。

-await msalInstance.initialize()
+;(async function () {
+  await msalInstance.initialize()
+})()
C:\maia\maia\samples\azure-ad-b2c-sample\auth-frontend\app\src\services\authentication\authentication-service.ts
  14:2  error  Promises must be awaited, end with a call to .catch, end with a call to .then with a rejection handler or be explicitly marked as ignored with the `void` operator  @typescript-eslint/no-floating-promises

✖ 1 problem (1 error, 0 warnings)

しかし IIFE を使用すると、頼りになるはずの no-floating-promise が、 2 行目の IIFE に対して警告を出力してしまいます。

そのときのためのオプション ignoreIIFE が用意されているので、trueに設定することで想定通りの挙動をしてくれるようになります。

vue-router の除外設定のように eslint.config.ts に設定しておくことも考えられますが、 AlesInfiny Maia/Maris のサンプルアプリでは対象箇所が 1 箇所のみだったこともあり、一連の流れを思い出しやすいように、 Top level await を使用していたモジュールのコードのみに設定を上書きしています。

+/* eslint @typescript-eslint/no-floating-promises: ["error", { "ignoreIIFE": true }] */

Typed Linting の欠点(パフォーマンス)

Typed Linting の抱える最大の欠点がパフォーマンスです。
下図に示した Linting の概念図を参考に、問題点を説明します。
左が通常の(Typed ではない) Linting 、中央が Typed Linting 、右が次のセクションで説明する Typed Linting の将来像です。

image.png

Typed ではない通常の Linting では、 TypeScript をパース(構文解析)して抽出した AST(抽象構文木)の情報だけあればルールの適用が可能です。

一方で、 Typed Lint を行うためには AST に加えてルールの適用に型情報が必要なので、追加で TypeScript コンパイラーを実行して型情報を生成する必要があります。

一例として、 create-vue のサンプルアプリケーションで処理時間を計測したところ、下表のようにデフォルト設定での Linting の実行時間が 7.0 秒であるのに対し、 Typed Linting の実行時間は 11.5 秒と、処理時間が 4.5 秒(64%) も増加しました。

同じアプリケーションに対して TypeScript コンパイラーの実行を含む型チェック処理を実行したところ、 5.6 秒を要したので、小規模なアプリケーションでは、 AST の抽出やルールの適用よりも、 型情報の生成にボトルネックがありそうです。

処理内容 ESLint の設定 処理時間
Linting vueTsConfigs.recommended 7.0 秒
Typed Linting vueTsConfigs.recommendedTypeChecked 11.5 秒
型チェック
( tsc --build --force )
- 5.6秒

一般的に解析対象のソースコードが増加するほど AST の抽出やルールの適用と、型情報の生成それぞれに時間を要するので、大規模プロジェクトでは Linting の実行待ちが深刻な問題になることが知られています。

大規模なアプリケーションについてはどちらがボトルネックになるかは実際の計測はできていませんが、 typescript-eslint の Performance に関する解説では、

ESLint のパフォーマンス低下の多くは、型情報対応の lint ルールが TypeScript の型チェック API を呼び出すことに起因しています。

と述べられています。

また、フランスのFintech 企業 Spiko 社の技術ブログでも、 3363 ファイル 24612 行の TypeScript に対して、 パースが約 0.7 秒に対し、型チェックが約 4.7 秒かかっているというデータが紹介されています。

そのため、大規模プロジェクトにおいても、 ASTの抽出やルールの適用よりも、型情報の生成にボトルネックがあると予想されます。

oxlint による高速 Typed Linting の可能性

このパフォーマンス問題の救世主と期待されるのが、 Rust 製の Linter である oxlint の存在です。 oxlint は ESLint の 50-100 倍の速度が出ると言われているので、パースと Linting のパフォーマンスを改善できます。

加えて、 2025 年 8 月 17 日に Typed Linting の機能がプレビューされました。公式ブログ によると、既存の typescript-eslint による Linting では 1 分かかっていたレポジトリの Linting が、 10 秒未満で完了するそうです。この背景には TypeScript コンパイラーを Go でリライトするプロジェクト が大きく関わっています。なぜなら、高速化された TypeScript コンパイラーを使用することで、ボトルネックとなっている型情報の生成のパフォーマンスを改善しているからです。

おわりに

Typed Linting は非同期処理に起因する不具合を早期に検出できる有効な手法であり、 Vue.js + TypeScript の開発において品質と効率を高める大きな助けとなります。パフォーマンス面の課題は残るものの、今後のツール進化による改善が期待され、新規・既存を問わず導入を検討する価値があるでしょう。

We Are Hiring

BIPROGY では一緒に働く仲間を募集しています。
ご興味がある方はこちらをご参照ください。
https://www.biprogy.com/recruit/recruiting/

  1. 当社がオープンソースで開発を進めているアプリケーションアーキテクチャに関するドキュメントおよびそれに従ったサンプルアプリケーションのこと。詳細はそれぞれ AlesInfiny MaiaAlesInfiny Maris の GitHub レポジトリを参照。

  2. この時点のブログでは v10 のリリースは 2024 年末か 2025 年初頭を見込んでいると記載があるが、 2025/9/1 時点で v10 のリリースは行われておらず、当初計画より遅れていることが推測される。

  3. 視認性をよくするため、掲載している eslint.config.ts 中の自動生成されるコメントは削除している。

  4. typescript-eslint の定義済みルールセットの種類とルールの詳細については Recommended Configurations を参照。本記事の設定の場合、より正確には @vue/eslint-config-typescript によってラップのうえ、 Vue.js 用にカスタマイズされたルールセットを使用している。

  5. AlesInfiny Maia/Maris のサンプルアプリケーションでは、モノレポ構成を取っている都合で追加の設定をしている。詳細は ESLint の設定 を参照。なおモノレポ構成で flat config の設定が扱いにくい問題については、GitHub 上での問題提起 の末 実験的機能 が実装され、 v10 で改善の見込みがある。

  6. 該当箇所のソースコード周辺のコメント を参照。(@vue/typescript-eslint v14.6.0 時点)

  7. Typed Linting のための parserOptions.projectService の設定方法については 公式ブログ が詳しい。

  8. Immediately Invoked Function Expression の略。 Top level await 機能の導入以前は await 演算子を async 関数中でしか使用できなかったため、このような記法で Top level await と同等の機能を実現する必要があった。

6
3
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
6
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?