A Worker config is data, so every binding name in it is a string that nothing checks. Wrangler can load a config that is a TypeScript program instead, behind a flag it still hides, and Cloudflare's new cf CLI loads the same file with no flag at all. Both infer the Worker's Env from it. This page measured that twice, in August and again on 2026-10-05. The upload still comes out byte-identical. The Durable Object type still degrades for a Worker that binds its own class, and now carries the class through when the binding names a second Worker. And one August claim was wrong: wrangler types already wrote a plain-text variable's value into the type.
The first run, on 2026-08-24, used wrangler 4.125.0 with @cloudflare/config 0.7.0. The second, on 2026-10-05, used wrangler 4.147.0 (the pkg.pr.new build at f025bbf this site pins), @cloudflare/config 0.23.0, cf 1.0.0-beta.12, TypeScript 7.0.2 and Node 26.10.0. Config 0.23.0 is what cf installs today, so it is what a reader gets. Every row was cross-checked on 0.20.0, this site's own pin, and came back the same except where a row says otherwise.
Where the two runs disagree, the page says so in place and keeps the August result beside the October one.
Here is the whole problem in one line. In August this site's Worker declared 21 bindings across two config files, and its request handler typed them like this:
@typedef {(request: Request, env: any, ctx: any, url: URL) => Response} RouteHandlerany. Every KV namespace, every D1 database, the R2 bucket, the Durable Object, all of it. Rename a binding in wrangler.jsonc and nothing in the tree goes red; the failure arrives at runtime as a property read on undefined, in production, on whichever route touched it first.
That is a property of the format. JSON cannot export a type. The usual repair is wrangler types, which reads the config and writes a .d.ts, and it works: it produces the full interface, Durable Object generics and all. On 4.125.0 and on 4.147.0 alike, it also prints this when it finishes.
Remember to rerun 'wrangler types' after you change your wrangler.jsonc file.A generated file plus a reminder is a snapshot plus a promise. The snapshot is correct on the day it is taken and silently wrong afterwards, which is the same shape as every committed artifact this site has replaced with a build step.
Wrangler loads a cloudflare.config.ts, and an optional wrangler.config.ts beside it, behind --x-new-config. On 4.147.0 the flag is still marked hidden: true, and it appears in the help of none of build, deploy, dev, types or versions upload. The package still describes itself as "not yet stable enough for external use".
Two things moved around the flag since August. Cloudflare now documents the file as an open beta, in a page last updated 2026-09-30, where in August a search found no documentation at all. And the documented door is cf, which imports the helpers from cf/config and, for a JavaScript Worker, hands the build to wrangler. On 2026-10-02 a wrangler maintainer answered workers-sdk#16002 with "we don't intend to support cloudflare.config.ts in more Wrangler commands" and recommended cf. So the flag keeps working where it works today, and new commands for this format arrive in cf.
The two filenames are the reverse of the guess. cloudflare.config.ts holds the Worker: name, compatibility date, entrypoint, triggers, bindings, exports, observability, placement, limits. wrangler.config.ts holds the tooling: the build command, the assets directory, dev settings, tsconfig, source maps. Put a Worker field in the tooling file and the error names the mistake and the fix, on 4.147.0 as on 4.125.0:
compatibilityDate is not a supported field in wrangler.config.ts. Move it to cloudflare.config.ts.Toggle bindings on the left. The middle pane is the config you would write, in the shape that builds today. The right pane switches between what the platform receives and what your env becomes. Every row below was measured through a real Worker on 2026-10-05: the resolved JSON is what wrangler build --x-new-config --x-cf-build-output wrote, and each type is what the compiler reported for that key of the generated interface.
The resolved pane is the Build Output Specification wrangler writes to .cloudflare/output/v0/workers/default/worker.config.json, which is the readable answer to "what did my config actually become". In August that file was config.json and opened with "type": "worker"; today it drops that key and ends with a manifest. The Env pane is what tsc reported for each key of the generated interface.
The two front doors agree. cf build (1.0.0-beta.12) printed "Delegating to Wrangler" and wrote the same four files wrangler build did, sha256 for sha256. The snippet imports from @cloudflare/config/public, as this site's own cloudflare.config.ts does. The documentation's cf/config re-exports that module in one line, so either import works, and the resolved output is identical.
One trap sits between the build and the types. The generated .cloudflare/types/index.d.ts imports cf/config. Install wrangler and @cloudflare/config without cf, keep skipLibCheck on as most templates do, and nothing complains: Env comes back empty, and every binding read is TS2339 on a value typed any. Only with skipLibCheck off does TS2307 name the missing module. Add cf as a dev dependency, as the docs say, or map cf/config in paths, which is what this site does.
Thirteen binding kinds went through the compiler. Most land where you would guess, and three carry more than a bare type.
bindings.text("default") gives env.AI_GATEWAY the type "default", so the variable's value is in the type and a comparison against a string that was never configured is a compile error. bindings.json({ dcz: true, tier: 2 }) comes back as { dcz: true; tier: number }, where the boolean narrows to a literal and the number widens, which is ordinary TypeScript inference showing through. And bindings.secret() types the secret as string, so a secret you forgot to set is a name the compiler already knows about.
This section used to say a JSON config structurally cannot do this. That was wrong, and it was already wrong on 4.125.0. Run against an equivalent wrangler.jsonc, wrangler types writes AI_GATEWAY: "default" on both 4.125.0 and 4.147.0. It types the required secret as string too, and it narrows the JSON variable further than inference does, to {"dcz":true,"tier":2}. What the TypeScript config changes is when the type is computed. wrangler types takes a snapshot that the reminder above asks you to retake. The inferred Env reads the config every time the compiler runs, so a renamed binding is a type error with no regeneration step.
One row is new since August. bindings.analyticsSQL(), added in config 0.22.0, resolves to {"type":"analytics"} and types as AnalyticsSQLBinding. Wrangler 4.147.0 builds it. cf 1.0.0-beta.5, which carries config 0.20.0, lets wrangler write the build output and then refuses that output on its own schema check, with "Invalid discriminator value", and exits 1. Beta.12 accepts it.
The Durable Object type is where the new format lost in August, and the October run found a way around it that works for some Workers and leaves the rest where they were. Against a JSON config, wrangler types threads the class through, on 4.147.0 as on 4.125.0:
COUNTER: DurableObjectNamespace<import("./src/index").Counter>;
BOOKING: Workflow<Parameters<import("./src/index").Booking['run']>[0]['payload']>;Under inference, a binding that names its Worker by string still comes back as DurableObjectNamespace<undefined>, so a stub method call fails with Property 'increment' does not exist on type 'DurableObjectStub<undefined>'. That holds with the entrypoint given as a path string and with it given as a module through the { type: "cf-worker" } import attribute, the same two forms August tried.
What changed is the binding helper. Its worker option (spelled workerName in August) now takes either a name or a Worker definition. Bind a class that a second Worker exports, pass that Worker's definition, and give that definition a module entrypoint:
import * as counterModule from "./src/index.ts" with { type: "cf-worker" };
export const counterWorker = defineWorker({
name: "probe-counter",
entrypoint: counterModule,
exports: { Counter: exports.durableObject({ storage: "sqlite" }) },
});
// in the binding Worker's env:
COUNTER: bindings.durableObject({ worker: counterWorker, exportName: "Counter" }),Now env.COUNTER types as DurableObjectNamespace<Counter>, and await stub.increment(1) is a number the compiler checks. A Workflow bound the same way types as Workflow<{ slot: string }>. The resolved JSON is the same as the string form, "worker": "probe-counter", so the definition buys a type and changes nothing the platform receives.
Two controls say which half does the work. The same definition with a path-string entrypoint gives DurableObjectNamespace<DurableObjectBranded>, which still has no methods, so the module import is load-bearing. And a Worker that binds its own class cannot take this route: pointing the binding at its own definition through a factory is circular, tsc reports TS7022, and the binding collapses to never. One more thing the build does not do is type-check the config. A Workflow binding whose exportName the definition did not declare was a compile error, and wrangler build still exited 0.
So the scoreboard moved. Cross-Worker Durable Objects and Workflows now type as precisely as wrangler types types them, and stay current without a regeneration step. A Worker that binds its own Durable Object, the common single-Worker case, still types worse than the snapshot does.
The cumulative migration list is gone. A class declares what it is on its own export, and the storage engine sits beside it:
| wrangler.toml | cloudflare.config.ts |
|---|---|
[[migrations]] tag = "v1" | Counter: exports.durableObject({ storage: "sqlite" }) |
deleted_classes = ["Old"] | Old: exports.durableObject({ state: "deleted" }) |
renamed_classes = [{ from, to }] | Old: exports.durableObject({ state: "renamed", renamedTo: "New" }) |
This is a different upload path. Wrangler 4.147.0 still decides between them in one function, resolveDoLifecyclePayload: when a config declares Durable Object exports it sends those exports and sends no migrations at all, and it computes a migration only for configs that declare none.
In August that left one question a dry run structurally cannot answer, because a dry run never computes a migration: would the API accept the export form for a class that already exists under an applied migration tag? This site's demo Worker answered it on 2026-09-21. It deployed on this format (wrangler 4.136.0, pin b168333) against a Counter created under tag = "v1", the API accepted it, and the counter read 18 and then 20 on the calls after, where a recreated namespace would have restarted from 0. That answer covers wrangler deploy. It says nothing about versions upload, which refuses Durable Object lifecycle changes on its own path.
The dependency instrumentation key still has no home. The old config set [dependencies_instrumentation] enabled = true, and config 0.23.0 has no field for it. The escape hatch offers no route around it, because unsafe carries metadata and capnp schemas alone. This cost nothing, for a reason only readable in wrangler's source: 4.147.0 still tests the key as enabled !== false, so an absent block behaves exactly like the explicit true. A dry run reports on none of this, which is why the answer came from reading the upload path.
The types file now comes from a build. wrangler types --x-new-config still fails, with "Unknown arguments: x-new-config, xNewConfig". In August the generated file came only from wrangler dev, so the inferred types existed on a machine somebody had run the dev server on. On 4.147.0, wrangler build --x-new-config --x-cf-build-output writes it too, and so do cf build and cf workers types, which wrote the same 622,861 bytes. That is the build step this page asked for in August.
The loader refuses bun, in a message that names itself, and on 4.147.0 a second warning follows it:
cloudflare.config.ts loading is not supported on Bun. Please use Node.js v22.18.0 or higher.
Wrangler does not support the Bun runtime. Please try this command again using Node.js via `npm` or `pnpm`.On a repository whose whole toolchain is bun, that reads as a blocker for about a minute. It is a claim about the process that ends up running wrangler, and the package manager that launched it does not enter into it. Re-run on bun 1.4.3-canary.1:
| invocation | result |
|---|---|
bun x --no-install wrangler … --x-new-config | loads, builds |
bun run <script>, resolving the same shim | loads |
bun run --bun <script> | refused |
bun ./node_modules/wrangler/bin/wrangler.js … | refused |
The first two resolve node_modules/.bin/wrangler, whose #!/usr/bin/env node shebang hands the process to node before any config is read. The last two put bun in front of the loader, one by overriding the shebang and one by running wrangler's entry file directly. So "does this tool support bun" turned out to be a question about a shebang, and the answer changed with the invocation while the tool stayed the same.
The conversion ran first on this site's smallest deployed Worker, the one behind /garage/cf/*, chosen because it is a demo that a person deploys by hand. In August the upload came out at 8.83 KiB, 3.07 KiB gzip, the same as before to the byte, with the same four bindings. Re-measured on 4.147.0 against the old wrangler.toml with its flags and minify setting brought level, both configs produce the same index.js, sha256 for sha256, at 5.94 KiB, 2.63 KiB gzip. The source maps differ only in their output-relative paths.
In August the main Worker still typed env as any, because converting it meant folding two config files of 24,662 and 10,987 bytes into one program and putting a hidden flag on the path that publishes production. It moved on 2026-09-28, with an upload byte-identical to the JSON's. Its handlers take env: Env now, from a hand-written interface that a check diffs against cloudflare.config.ts in both directions. That choice is about optional secrets, which the interface marks with ? so every read has to face their absence, and about the Durable Object: its Counter lives in a second Worker that still has a wrangler.jsonc, so there is no definition to import yet.
Two of August's three triggers have fired. The API accepted the export-based Durable Object form on a real deploy, so the migration table is a translation. Documentation arrived, though the flag stayed hidden and the documented door became cf.
The third half-fired: the class type now threads through a binding to another Worker. What would finish it is a self-bound Durable Object that types as its class, which would remove the last column where wrangler types wins. The other result worth watching is the format leaving open beta, since several shapes on this page moved between the two runs: the import path, the default export, the Durable Object binding key and the build output file.