HonoでCloudflare D1を使う|TypeScript Bindingsの型定義の書き方

HonoでCloudflare D1を使う|TypeScript Bindingsの型定義の書き方

※本記事にはプロモーション(広告・アフィリエイトリンク)を含みます。

HonoでCloudflare D1を使おうとしたとき、TypeScriptの型エラーでつまずきやすい。「c.env.DBがany型になる」「D1Databaseが見つからない」という問題は、Bindingsの型定義とwrangler.tomlの設定が正しく噛み合っていないことが原因だ。この記事では、wrangler.tomlの設定からTypeScript型定義の書き方、Honoへの渡し方まで順番に解説する。

D1バインディングとHonoのBindings型の関係

Cloudflare WorkersでD1を使うとき、wrangler.tomlに[[d1_databases]]セクションを書くことで、Workersランタイムが実行時にenv.DB(bindingプロパティ名)へD1Databaseオブジェクトを注入する。

HonoはWorkers向けのWebフレームワークで、このenvオブジェクトをc.envとして受け取る設計になっている。型を付けるには「この環境にはどんなバインディングがあるか」をジェネリクスで教える必要があり、省略するとc.envはanyになってしまう。

場所 役割
wrangler.toml ランタイムに何をバインドするか宣言する
TypeScript型定義 コンパイル時の型チェックを通す
Honoのジェネリクス c.envにその型を伝える

この3つが揃って初めて、c.env.DB.prepare(...) にIDEの補完が効き、型安全なコードが書けるようになる。

wrangler.tomlでD1バインディングを設定する

まずwrangler.tomlに以下を追加する。database_idはD1データベース作成時に取得したUUIDに置き換える。

name = "my-worker"
main = "src/index.ts"
compatibility_date = "2024-01-01"

[[d1_databases]]
binding = "DB"
database_name = "my-database"
database_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"

binding = "DB" の値が後でTypeScript側のプロパティ名と一致しなければならない。たとえば binding = "USERS_DB" と書いたなら、型定義側でも USERS_DB: D1Database とする。名前の不一致が原因で実行時にundefinedになるトラブルは起きやすいため、必ずどちらかに揃えること。

ローカル開発時は npx wrangler dev で起動すると、ローカルのSQLiteファイルを使ってD1が動作する(執筆時点のwrangler v3以降では--localがデフォルト。--remoteフラグを付けると本番のD1に接続する)。

TypeScript型定義の書き方

@cloudflare/workers-typesをインストールする

D1DatabaseなどWorkersランタイム固有の型は、@cloudflare/workers-typesパッケージに含まれている。

npm install --save-dev @cloudflare/workers-types

インストール後、tsconfig.jsonのcompilerOptions.typesに追加する。

{
  "compilerOptions": {
    "target": "ES2022",
    "module": "ES2022",
    "lib": ["ES2022"],
    "types": ["@cloudflare/workers-types"]
  }
}

これを忘れるとD1Databaseが型として認識されず、型定義ファイルにエラーが出る。

Envインターフェースを手書きする

wrangler.tomlのbinding名と一致するプロパティを持つインターフェースを定義する。

// src/types.ts
export interface Env {
  DB: D1Database;
  // 複数のバインディングがあれば続けて追加
  // CACHE: KVNamespace;
  // ASSETS: R2Bucket;
}

wrangler typesコマンドで自動生成する方法

手書きの代わりに、wrangler typesコマンドを使うとworker-configuration.d.tsが自動生成される。

npx wrangler types

生成ファイルには interface Env { DB: D1Database; } のような内容が含まれるため、プロパティ名の書き間違いを防げる。wrangler.tomlを変更するたびに再実行が必要なため、CI/CDパイプラインの先頭ステップに組み込んでおくと安全だ。

HonoにBindings型を渡す

Honoのジェネリクスは Hono<{ Bindings: Env }> という形で書く。Bindingsというキーを使うのがポイントで、Hono<Env> と直接書いてもc.envに型は伝わらない。詰まりやすいポイントだ。

// src/index.ts
import { Hono } from 'hono'
import type { Env } from './types'

// Bindings キーに渡すのが必須
const app = new Hono<{ Bindings: Env }>()

export default app

HonoのジェネリクスにはBindingsのほかにVariablesもある。Variablesはc.set()とc.get()でルート間を横断して値を共有する用途(ミドルウェアで取り出した認証情報など)に使う。D1を含む外部バインディングは必ずBindingsに入れること。

// BindingsとVariablesを両方使う場合
const app = new Hono<{
  Bindings: Env
  Variables: {
    userId: string
  }
}>()

c.env.DBでD1を操作するコード例

型が正しく設定できたら、c.env.DBでD1Databaseにアクセスできる。D1のAPIは.prepare(sql).all()・.prepare(sql).first()・.prepare(sql).run()の3つが基本だ。

// GETルート:一覧取得
app.get('/users', async (c) => {
  const { results } = await c.env.DB
    .prepare('SELECT id, name FROM users ORDER BY id DESC LIMIT 20')
    .all<{ id: number; name: string }>()

  return c.json(results)
})

// POSTルート:1件挿入
app.post('/users', async (c) => {
  const { name } = await c.req.json<{ name: string }>()

  await c.env.DB
    .prepare('INSERT INTO users (name) VALUES (?)')
    .bind(name)
    .run()

  return c.json({ ok: true }, 201)
})

// GETルート:1件取得(存在しない場合は404)
app.get('/users/:id', async (c) => {
  const id = c.req.param('id')
  const row = await c.env.DB
    .prepare('SELECT id, name FROM users WHERE id = ?')
    .bind(id)
    .first<{ id: number; name: string }>()

  if (!row) return c.notFound()
  return c.json(row)
})

.all<T>() のようにジェネリクスで行の型を指定すると results が T[] 型になる。型引数を省略すると Record<string, unknown>[] になり、以降の処理でキャストが増えるため、型を指定する習慣をつけるとよい。

よくあるつまずきポイント

症状 原因 対処
c.env が never 型 Hono<Env> と直接書いている Hono<{ Bindings: Env }> に変える
D1Database が見つからない @cloudflare/workers-types 未設定 インストール後tsconfig.jsonに追加
実行時に c.env.DB がundefined binding名がwrangler.tomlと不一致 両方を同じ名前に揃える
ローカルでDB操作がエラー wrangler dev を経由せず直接実行 npx wrangler dev で起動する

まとめ

HonoでD1を型安全に扱うには4つのステップが必要だ。①wrangler.tomlで[[d1_databases]]を宣言する、②@cloudflare/workers-typesをインストールしてtsconfig.jsonに追加する、③interface Env { DB: D1Database }を定義する、④Hono<{ Bindings: Env }>とジェネリクスに渡す。

まず手元でnpx wrangler typesを一度実行して、生成されるworker-configuration.d.tsの内容を確認してみよう。binding名が自動で反映されるため、手書きとの食い違いがすぐ把握できる。型が通ったあとは.prepare().all<T>()でSQLの戻り値にも型がつき、IDEの補完が格段に使いやすくなる。

この記事で触れたもの

よくある質問

HonoでD1Databaseの型が見つからないときの原因は?

@cloudflare/workers-typesがインストールされていないか、tsconfig.jsonのcompilerOptions.typesに追加されていないことが多い。npm install --save-devでインストール後、tsconfig.jsonのtypesに@cloudflare/workers-typesを追加すると解消される。

Hono<Env>とHono<{ Bindings: Env }>の違いは何ですか?

Hono<Env>と書いても型エラーにはならないが、c.envにEnvの型が伝わらずanyやneverになる。HonoはジェネリクスにオブジェクトでBindings・Variablesを受け取る設計のため、バインディング型は必ずBindingsキーに渡す必要がある。

wrangler typesコマンドは何のために使うのですか?

wrangler.tomlに定義したバインディングからTypeScript型定義ファイル(worker-configuration.d.ts)を自動生成するコマンド。手書きするとbinding名の誤字やプロパティ名不一致が起きやすいため、wrangler.tomlを変更するたびに実行すると安全に型を保てる。