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を変更するたびに実行すると安全に型を保てる。