This article is an English translation of the original Japanese article.
I deploy a Next.js application to Cloudflare Workers through @opennextjs/cloudflare. After running wrangler types to type the D1 binding, TypeScript errors appeared across the application.
error TS18046: 'body' is of type 'unknown'.
error TS2339: Property 'name' does not exist on type 'object'.
The main symptom was that request.json() suddenly returned unknown. In my setup, Wrangler's generated runtime definitions collided with the DOM types used by Next.js. I fixed it by removing the generated file and writing a small declaration file that uses import type. The exact behavior can vary with versions and tsconfig scope, so this article describes the configuration where I reproduced it.
What happened
By default, wrangler types creates worker-configuration.d.ts. It contains both the binding Env interface and the complete Workers runtime definitions. Global types such as Request, Response, and Body all enter the project.
In the Workers runtime types, Body#json() returns Promise<unknown>. In the DOM library used by Next.js, it returns Promise<any>. Once the generated runtime globals joined the same TypeScript project, every await request.json() in a Route Handler became unknown, and accesses such as body.name failed type checking.
Those Workers types are appropriate in a Worker-only project. A Next.js application also needs the DOM library, and both libraries define the same global names.
Attempt: --include-runtime=false
Wrangler can exclude runtime types from its generated output.
wrangler types cloudflare-env.d.ts --env-interface CloudflareEnv --include-runtime=false
The resulting file contains only the environment interface.
// Generated cloudflare-env.d.ts
interface CloudflareEnv {
DB: D1Database;
ASSETS: Fetcher;
}
This looked promising, but it introduced another error.
error TS2552: Cannot find name 'D1Database'. Did you mean 'IDBDatabase'?
D1Database and Fetcher are global names supplied by the runtime definitions. Including the runtime caused the DOM collision, while excluding it left the generated interface unable to resolve those names.
Fix: a handwritten declaration file with import type
I stopped generating the file and imported only the binding types I needed.
npm i -D @cloudflare/workers-types
// cloudflare-env.d.ts, written manually
import type { D1Database, Fetcher } from "@cloudflare/workers-types";
declare global {
interface CloudflareEnv {
DB: D1Database;
ASSETS: Fetcher;
}
}
export {};
This works for three reasons:
- Types brought in with
import typestay scoped to this module. The Workers versions ofRequestandResponsedo not leak into the global namespace or collide with the DOM library. -
declare globalexposes onlyCloudflareEnv.getCloudflareContext()from@opennextjs/cloudflarereturns itsenvusing this interface, which givesenv.DBtheD1Databasetype. - The import already makes the declaration file a module. The final
export {}is included to make that status explicit, but it is not required.
The binding type is then available at the call site.
import { getCloudflareContext } from "@opennextjs/cloudflare";
function getDb() {
return getCloudflareContext().env.DB; // inferred as D1Database
}
Tradeoff
The handwritten file must be updated when a binding is added. This gives up the main benefit of wrangler types, which generates an interface from wrangler.jsonc.
For my application, the bindings change infrequently and consist of one D1 database plus static assets. Maintaining a few lines in one file has not been a problem. A larger project with many bindings could generate CloudflareEnv and then transform the output to add scoped type imports.
Summary
- Wrangler's generated file can include Workers runtime globals that collide with the DOM library in a Next.js project. A sudden
unknownreturn type fromjson()is one symptom. - In my configuration,
--include-runtime=falseleft binding types such asD1Databaseunresolved. - A handwritten declaration file using
import typeanddeclare globaltypes the bindings without adding Workers request and response globals.
Worker-only projects do not have the same DOM coexistence problem, which made the cause harder to find.
Environment
-
@opennextjs/cloudflare1.20, Wrangler 4.113, and@cloudflare/workers-types5.x - Next.js 16 App Router. Next.js 15 can have the same type structure
Top comments (0)