Cloudflare Workersプロジェクトで、R2ストレージを扱う関数を実装していた。
TypeScriptとしては正常にコンパイルできるが、ESLintがR2Object型を認識せず、no-undefエラーが発生した。

正直なところ、このエラーの原因を特定するのに少し時間がかかった。

TypeScriptの型システムは完璧に動作しているのに、ESLintだけがエラーを出す——この状況は多くの開発者が直面する問題です。

こんな人におすすめ

この記事は以下のような方に役立ちます。

  • Cloudflare Workers + R2 で開発をしている
  • ESLintのno-undefエラーに遭遇して困っている
  • TypeScriptのグローバル型とESLintの違いを理解したい
  • 型安全性を保ちながらLintエラーを回避したい

目次

症状

/functions/api/storage/images/list.ts
  214:42  error  'R2Object' is not defined  no-undef

@cloudflare/workers-typesのtriple-slash directiveを追加しても解決しなかった。

/// <reference types="@cloudflare/workers-types" />

個人的に、この対処法が効かないときは「どうすればいいんだ?」と少し焦りました。

原因

ESLintとTypeScriptの仕組みの違い

ESLintはTypeScriptのtriple-slash directiveで定義されたグローバル型を認識しません。
TypeScriptコンパイラは認識するが、ESLintのno-undefルールは別の仕組みで動作するのです。

なぜこの問題が発生するのか

TypeScriptのtriple-slash directive(/// <reference types="..." />)は、TypeScriptコンパイラ専用の機能です。
これはコンパイル時にのみ参照され、ESLintのようなリンティングツールからは見えません。

ESLintのno-undefルールは、ESLintのドキュメントにある通り、グローバル変数の定義をglobals設定またはexportedコメントで認識します。
TypeScriptの型定義ファイル(.d.ts)で宣言されたグローバル型は、ESLintからは見えないのです。

詳細は、@typescript-eslint/parserのドキュメントで確認できます。

直し方

どの解決策を選ぶべきか、以下のフローチャートで判断できます。

flowchart TD
    Start[ESLintのno-undefエラーが発生] --> Q1{型安全性を維持したい?}

    Q1 -->|はい| A1[解決策1: インターフェース定義]
    Q1 -->|いいえ| Q2{型チェックが不要でOK?}

    Q2 -->|はい| A2[解決策2: no-undef無効化]
    Q2 -->|いいえ| A3[解決策3: globals設定]

    A1 --> R1[✅ 推奨: 型安全性を保てる]
    A2 --> R2[⚠️ 注意: 型チェックが緩くなる]
    A3 --> R3[⚠️ 注意: 型情報が失われる]

    style A1 fill:#c8e6c9
    style A2 fill:#fff9c4
    style A3 fill:#fff9c4
    style R1 fill:#c8e6c9
    style R2 fill:#ffe0b2
    style R3 fill:#ffe0b2

解決策1: インターフェースを明示的に定義する(推奨)

必要なプロパティのみを持つインターフェースを明示的に定義します。

/**
 * R2オブジェクトの必要なプロパティのみを定義
 * (ESLint対応: @cloudflare/workers-typesのグローバル型はESLintに認識されないため)
 */
interface R2ObjectLike {
  key: string;
  uploaded: Date;
  size: number;
}

export function parseImageObject(object: R2ObjectLike, r2PublicUrl: string): ImageInfo | null {
  // ...
}

実際のR2Object型と互換性があるため、env.STORAGE.list()の結果をそのまま渡せます。

以下の図は、R2から取得したデータがどうやって関数に渡されるかを示したフローです。

sequenceDiagram
    participant R2 as R2ストレージ
    participant Env as env.STORAGE
    participant Func as parseImageObject関数
    participant User as ユーザーコード

    R2->>Env: list()でR2Objectsを取得
    Env->>Func: R2Object[]を渡す
    Note over Func: interface R2ObjectLike<br/>で型チェック✅
    Func->>User: ImageInfo[]を返す
    Note over User: ESLintエラーなし✅

この方法で型安全性も保ちながら、ESLintエラーを回避できました。

解決策2: ESLintのno-undefルールを無効化する

もう一つの方法は、@typescript-eslint/no-undefを使用するか、no-undefルールを無効化することです。
ただし、この方法は型チェックが緩くなる可能性があるため推奨しません。

/* eslint-disable no-undef */
// R2Objectを使用するコード
/* eslint-enable no-undef */

解決策3: globals設定で宣言する

.eslintrc.jsonglobals設定にR2Objectを追加する方法もあります。

{
  "globals": {
    "R2Object": "readonly"
  }
}

しかし、この方法では型情報が失われるため、解決策1のインターフェース定義が最も安全です。

学び

  • ESLintとTypeScriptは型認識の仕組みが異なる
  • グローバル型に依存する場合、ESLint用に明示的な型定義が必要なことがある
  • 必要なプロパティのみを持つインターフェースを定義することで、型安全性を保ちながらESLintエラーを回避できる

よくある質問(FAQ)

Q. ESLintとTypeScriptのどちらを優先すべきですか?

A. 両方のエラーを解消するのが理想です。
TypeScriptの型チェックは実行時エラーを防ぐために重要です。
一方で、ESLintはコードの一貫性を保つために役立ちます。
インターフェース定義を使えば、両方の要件を満たせます。

Q. 他のCloudflare Workers型でも同様の問題が発生しますか?

A. はい、同様の問題が発生する可能性があります
R2BucketEnvExecutionContextなど、@cloudflare/workers-typesで定義されるグローバル型すべてで同様の問題が発生します。
同じアプローチでインターフェースを定義することで解決できます。

Q. インターフェース定義が面倒です。もっと簡単な方法はありませんか?

A. no-undefルールを無効化する方法がありますが、推奨しません。
型チェックの緩和はバグの原因になる可能性があるため、最初からインターフェースを定義することをおすすめします。

まとめ

今回の問題から得た教訓をまとめます。

  1. ESLintとTypeScriptは別物

    • ESLintはTypeScriptのtriple-slash directiveを認識しません
  2. 明示的な型定義で解決

    • 必要なプロパティのみを持つインターフェースを定義することで、両方の要件を満たせます
  3. 実用的なアプローチ

    • 実際の型と互換性があるため、既存のコードにそのまま適用できます
  4. 同じ問題が他の型でも発生

    • Cloudflare Workersのグローバル型すべてで同様の問題が発生する可能性があります

この方法を採用したことで、Lintエラーが解消され、開発体験が改善されました。

確認手順

  1. npm run lint でエラーがないことを確認
  2. npm run test でテストが通ることを確認
  3. TypeScriptコンパイルエラーがないことを確認

参考リンク