Generated types
sproutboat types writes worker-configuration.d.ts with an Env interface, like wrangler types, so env and the handler are fully typed.
sproutboat types reads sproutboat.jsonc and writes worker-configuration.d.ts, a declaration file with an Env interface: one property per binding, typed by kind. It is the file wrangler types writes, with the same name, the same Env and the same flags, so a handler is typed the way a Cloudflare Worker is.
npx sproutboat typespnpm sproutboat typesbunx sproutboat typesyarn sproutboat typesFor a project with a var, a KV namespace, a D1 database, a queue and a Durable Object, the file is:
/* eslint-disable */
// Generated by Sproutboat by running `sproutboat types` (hash: 298edcf4cd41ae74ab2358dd8990c971)
/// <reference types="sproutboat/types" />
interface __BaseEnv {
APP_URL: "https://hello.example.com";
SESSIONS: KVNamespaceAsync;
DB: D1DatabaseAsync;
JOBS: Queue;
COUNTER: DurableObjectNamespaceAsync<import("./src/index").Counter>;
}
declare namespace Cloudflare {
interface Env extends __BaseEnv {}
}
interface Env extends __BaseEnv {}
Commit it. The hash in the header is what --check compares, so reformatting the file does not make it stale.
Set up tsconfig
Add the generated file to include, next to your source. This is a tsconfig.json for a Sproutboat project; it is the one the examples use:
{
"compilerOptions": {
"target": "ES2022",
"module": "ESNext",
"moduleResolution": "bundler",
"lib": ["ES2022", "DOM", "DOM.Iterable"],
"types": [],
"strict": true,
"noEmit": true,
"skipLibCheck": true
},
"include": ["src", "worker-configuration.d.ts"]
}
"types": [] keeps unrelated @types/* packages (Node’s, for one) out of the program, since the handler has no Node globals. Sproutboat bundles the entry and strips types without checking them, so type errors never fail sproutboat build. Run tsc yourself, in a script or in CI, to make them fail:
{
"scripts": {
"types": "sproutboat types",
"typecheck": "sproutboat types --check && tsc"
}
}
Type the handler
Annotate the default export with satisfies ExportedHandler<Env>. request, env, ctx, controller and batch are then inferred, and env.SESSIONS and the rest autocomplete and are checked:
export default {
async fetch(request, env, ctx) {
const visits = Number((await env.SESSIONS.get("visits")) ?? 0) + 1;
await env.SESSIONS.put("visits", String(visits));
return Response.json({ visits, app: env.APP_URL });
},
async scheduled(controller, env, ctx) {
await env.SESSIONS.put("last-run", new Date(controller.scheduledTime).toISOString());
},
async queue(batch, env, ctx) {
for (const message of batch.messages) message.ack();
},
} satisfies ExportedHandler<Env>;
satisfies checks the object against ExportedHandler<Env> and keeps the object’s own type, as in Cloudflare’s templates. A handler with a queue consumer names the message body as the second type argument: satisfies ExportedHandler<Env, Job>.
The same checks catch the mistakes that only show up at run time otherwise: a binding name that does not exist, a Durable Object method that does not exist, a var compared with the wrong literal, and a binding call that is not awaited.
const value = env.SESSIONS.get("k"); // forgot the await: Promise<string | null>
if (value === "v") {
// error TS2367: This comparison appears to be unintentional because the types
// 'Promise<string | null>' and '"v"' have no overlap.
}
Reading env outside a handler
The generated file also declares Cloudflare.Env, which import { env } from "cloudflare:workers" reads, so a helper can use the bindings without taking env as a parameter:
import { env } from "cloudflare:workers";
export async function visits(): Promise<number> {
return Number((await env.SESSIONS.get("visits")) ?? 0);
}
Durable Objects and service bindings
A Durable Object binding is typed with its class when the entry is a TypeScript file that exports it, so a call over RPC is checked:
const counter = env.COUNTER.getByName("global");
const n: number = await counter.increment();
A service binding is FetcherAsync. The generator cannot read another project’s classes, so for RPC name the callee’s class where you use it: FetcherAsync<QuoteService>. See Service bindings.
Types follow the compatibility date
The types describe what the runtime does at your compatibility_date, so the file changes when you move the date:
| Binding | Type from the date | Type before it |
|---|---|---|
| KV | KVNamespaceAsync from 2026-10-25 |
KVNamespace (synchronous) |
| D1 | D1DatabaseAsync from 2026-10-24 |
D1Database (synchronous) |
| R2 | R2BucketAsync from 2026-10-26 |
R2Bucket (synchronous) |
| Durable Objects | DurableObjectNamespaceAsync from 2026-10-28 |
DurableObjectNamespace (synchronous) |
| Service and assets bindings | FetcherAsync from 2026-10-22 |
Fetcher |
await works at every date, so a handler written with await compiles and runs on both sides of each. From 2026-09-28 the file declares no global env, because the handler receives it as an argument; an earlier project still gets the global it needs.
Flags
sproutboat types [path] [--env-interface <name>] [--strict-vars[=false]] [--include-runtime[=false]] [--include-env[=false]] [--check]
| Flag | Does |
|---|---|
path |
The declaration file to write. Default worker-configuration.d.ts, next to sproutboat.jsonc. A path that is an existing directory is still read as the project directory, as earlier releases did. |
--env-interface <name> |
Name the interface something other than Env: --env-interface=MyEnv, then satisfies ExportedHandler<MyEnv>. |
--strict-vars |
On by default: a var’s type is its literal value ("https://hello.example.com"). --strict-vars=false types it as string. |
--include-runtime |
On by default: the file references the runtime types the CLI ships (sproutboat/types). --include-runtime=false leaves the reference out, for a project that supplies its own. |
--include-env |
On by default: the file declares Env. --include-env=false leaves it out. |
--check |
Write nothing. Exit 1 when the file is missing or out of date, by the hash in its header. For CI. |
Each flag takes --flag=value, --flag value or --no-flag, as in Wrangler. Two differences from wrangler types are on purpose: --include-runtime references the types the CLI ships instead of copying them into the file, and there is no --include-main-module, because there is no ctx.exports to type.
Keeping it current
dev and deploy rewrite worker-configuration.d.ts when the config has moved on, so a binding you just added shows up in your editor without asking. They do it only when the file was written by a plain sproutboat types; a file made with other flags (or by hand) is left alone, as Wrangler leaves it. They create the file only in a project that has a tsconfig.json, so a plain JavaScript project gets none.
Migrating from sproutboat-env.d.ts
Earlier releases wrote sproutboat-env.d.ts, which declared a global env for every date, and took a project directory as the argument. sproutboat types now deletes sproutboat-env.d.ts when it is still exactly as the old command generated it. A copy you edited stays where it is.
To move over:
- Run
sproutboat types, and commitworker-configuration.d.ts. - In
tsconfig.json, replacesproutboat-env.d.tswithworker-configuration.d.tsininclude. - Change
satisfies WorkersHandler<typeof env>tosatisfies ExportedHandler<Env>, and readenvfrom the handler’s second argument instead of the global.
What the types do not offer
Some parts of the Workers API are not in the runtime yet, so the types leave them out: ctx.passThroughOnException(), ctx.props on a handler’s ctx, and URLPattern. A handler that stays inside the types runs unchanged on Cloudflare.