One source of truth for Workers types

Keeping Cloudflare Workers type definitions accurate has historically been a manual process. The workers-types repository was updated by hand whenever runtime APIs changed, and browser API types were added liberally even when the Workers runtime didn't support them. The result: code that type-checked fine but threw at runtime when it hit an unsupported browser API.

That workflow is gone. An automated pipeline, built during a summer internship, now runs on every Workers runtime build. It generates TypeScript and Rust types plus an intermediate representation (IR) in JSON, syncs the output to the relevant repositories, and files PRs automatically when the runtime surface changes (Automatically generating types for Cloudflare Workers shows one such bot-generated PR).

Getting started with generated types

The quickest path is to scaffold a new project with wrangler:

$ wrangler generate my-typescript-worker https://github.com/cloudflare/worker-typescript-template

For an existing TypeScript project, install the current workers-types package:

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

then register it in your tsconfig.json:

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

After that, your editor will pick up completed type information automatically.

What's under the hood

The runtime codebase declares its public APIs with attributes like those in

class Blob: public js::Object {
public:
  typedef kj::Array<kj::OneOf<kj::Array<const byte>, kj::String, js::Ref<Blob>>> Bits;
  struct Options {
    js::Optional<kj::String> type;
    JS_STRUCT(type);
  };

  static js::Ref<Blob> constructor(js::Optional<Bits> bits, js::Optional<Options> options);
  
  int getSize();
  js::Ref<Blob> slice(js::Optional<int> start, js::Optional<int> end);

  JS_RESOURCE_TYPE(Blob) {
    JS_READONLY_PROPERTY(size, getSize);
    JS_METHOD(slice);
  }
};
. A Python script parses these declarations during each build into an abstract syntax tree (AST) containing the function identifier, argument types, and return types. From that parsing step, the IR that feeds both the Rust and TypeScript generators is produced (
{
  "name": "Blob",
  "kind": "class",
  "members": [
    {
      "name": "size",
      "type": {
        "name": "integer"
      },
      "readonly": true
    },
    {
      "name": "slice",
      "type": {
        "params": [
          {
            "name": "start",
            "type": {
              "name": "integer",
              "optional": true
            }
          },
          {
            "name": "end",
            "type": {
              "name": "integer",
              "optional": true
            }
          }
        ],
        "returns": {
          "name": "Blob"
        }
      }
    }
  ]
}
). The IR and type schema are both published to the workers-types repository, making the data available for anyone building type generators for other languages.

When the generator needs help

Generics and function overloads exist in TypeScript but not in the C++-based runtime, so the generated types can't always capture the intended API surface. For those cases, partial declaration overrides are applied on top of the generated output — DurableObjectStorage's generic get/set methods are a typical example (

declare abstract class DurableObjectStorage {
	 get<T = unknown>(key: string, options?: DurableObjectStorageOperationsGetOptions): Promise<T | undefined>;
	 get<T = unknown>(keys: string[], options?: DurableObjectStorageOperationsGetOptions): Promise<Map<string, T>>;
	 
	 list<T = unknown>(options?: DurableObjectStorageOperationsListOptions): Promise<Map<string, T>>;
	 
	 put<T>(key: string, value: T, options?: DurableObjectStorageOperationsPutOptions): Promise<void>;
	 put<T>(entries: Record<string, T>, options?: DurableObjectStorageOperationsPutOptions): Promise<void>;
	 
	 delete(key: string, options?: DurableObjectStorageOperationsPutOptions): Promise<boolean>;
	 delete(keys: string[], options?: DurableObjectStorageOperationsPutOptions): Promise<number>;
	 
	 transaction<T>(closure: (txn: DurableObjectTransaction) => Promise<T>): Promise<T>;
	}
documents another override example).

Rolling your own bindings

The IR in workers.json is a normalized description of each declaration: identifiers, argument and return types, and error information. The accompanying schema defines the format. One concrete use: generating external declarations for a WebAssembly-targeting language so you import exactly the runtime calls available.