TypeScript makes it easy for developers to write code that doesn’t crash, by catching type errors before your program runs. We want developers to take advantage of this tooling, which is why one year ago, we built a system to automatically generate TypeScript types for the Cloudflare Workers runtime. This enabled developers to see code completions in their IDEs for Workers APIs, and to type check code before deploying. Each week, a new version of the types would be published, reflecting the most recent changes.
Over the past year, we’ve received lots of feedback from customers and internal teams on how we could improve our types. With the switch to the Bazel build system in preparation for open-sourcing the runtime, we saw an opportunity to rebuild our types to be more accurate, easier to use, and simpler to generate. Today, we’re excited to announce the next major release of @cloudflare/workers-types
with a bunch of new features, and the open-sourcing of the fully-rewritten automatic generation scripts.
How to use TypeScript with Workers
Setting up TypeScript in Workers is easy! If you’re just getting started with Workers, install Node.js, then run npx wrangler init
in your terminal to generate a new project. If you have an existing Workers project and want to take advantage of our improved typings, install the latest versions of TypeScript and @cloudflare/workers-types
with npm install --save-dev typescript @cloudflare/workers-types@latest
, then create a tsconfig.json
file with the following contents:
{
"compilerOptions": {
"target": "esnext",
"module": "esnext",
"lib": ["esnext"],
"types": ["@cloudflare/workers-types"]
}
}
Your editor will now highlight issues and give you code completions as you type, leading to a less error-prone and more enjoyable developer experience.
Editor highlighting incorrect use of set
instead of put
, and providing code completions
Improved interoperability with standard types
Cloudflare Workers implement many of the same runtime APIs as browsers, and we’re working to improve our standards compliance even more with the WinterCG. However, there will always be fundamental differences between what browsers and Workers can do. For example, browsers can play audio files, whereas Workers have direct access to Cloudflare’s network for storing globally-distributed data. This mismatch means that the runtime APIs and types provided by each platform are different, which in turn makes it difficult to use Workers types with frameworks, like Remix, that run the same files on the Cloudflare network and in the browser. These files need to be type-checked against lib.dom.d.ts
, which is incompatible with our types.
To solve this problem, we now generate a separate version of our types that can be selectively imported, without having to include @cloudflare/workers-types
in your tsconfig.json
’s types
field. Here’s an example of what this looks like:
import type { KVNamespace } from "@cloudflare/workers-types";
declare const USERS_NAMESPACE: KVNamespace;
In addition, we automatically generate a diff of our types against TypeScript’s lib.webworker.d.ts
. Going forward, we’ll use this to identify areas where we can further improve our spec-compliance.
Improved compatibility with compatibility dates
Cloudflare maintains strong backwards compatibility promises for all the APIs we provide. We use compatibility flags and dates to make breaking changes in a backwards-compatible way. Sometimes these compatibility flags change the types. For example, the global_navigator
flag adds a new navigator
global, and the url_standard
flag changes the URLSearchParams
constructor signature.
We now allow you to select the version of the types that matches your compatibility date, so you can be sure you’re not using features that won’t be supported at runtime.
{
"compilerOptions": {
...
"types": ["@cloudflare/workers-types/2022-08-04"]
}
}
Improved integration with Wrangler
In addition to compatibility dates, your Worker environment configuration also impacts the runtime and type API surface. If you have bindings such as KV namespaces or R2 buckets configured in your wrangler.toml
, these need to be reflected in TypeScript types. Similarly, custom text, data and WebAssembly module rules need to be declared so TypeScript knows the types of exports. Previously, it was up to you to create a separate ambient TypeScript file containing these declarations.
To keep wrangler.toml
as the single source of truth, you can now run npx wrangler types
to generate this file automatically.
For example, the following wrangler.toml
…
kv_namespaces = [{ binding = "MY_NAMESPACE", id = "..." }]
rules = [{ type = "Text", globs = ["**/*.txt"] }]
…generates these ambient types:
interface Env {
MY_NAMESPACE: KVNamespace;
}
declare module "*.txt" {
const value: string;
export default value;
}
Improved integrated documentation and changelogs
Code completions provide a great way for developers new to the Workers platform to explore the API surface. We now include the documentation for standard APIs from TypeScript’s official types in our types. We’re also starting the process of bringing docs for Cloudflare specific APIs into them too.
For developers already using the Workers platform, it can be difficult to see how types are changing with each release of @cloudflare/workers-types
. To avoid type errors and highlight new features, we now generate a detailed changelog with each release that splits out new, changed and removed definitions.
How does type generation work under the hood?
As mentioned earlier, we’ve completely rebuilt the automatic type generation scripts to be more reliable, extensible and maintainable. This means developers will get improved types as soon as new versions of the runtime are published. Our system now uses workerd
’s new runtime-type-information (RTTI) system to query types of Workers runtime APIs, rather than attempting to extract this information from parsed C++ ASTs.
// Encode the KV namespace type without any compatibility flags enabled
CompatibilityFlags::Reader flags = {};
auto builder = rtti::Builder(flags);
auto type = builder.structure<KvNamespace>();
capnp::TextCodec codec;
auto encoded = codec.encode(type);
KJ_DBG(encoded); // (name = "KvNamespace", members = [ ... ], ...)
We then pass this RTTI to a TypeScript program that uses the TypeScript Compiler API to generate declarations and perform AST transformations to tidy them up. This is built into workerd
’s Bazel build system, meaning generating types is now a single bazel build //types:types
command. We leverage Bazel’s cache to rebuild as little as possible during generation.
import ts, { factory as f } from "typescript";
const keyParameter = f.createParameterDeclaration(
/* decorators */ undefined,
/* modifiers */ undefined,
/* dotDotDotToken */ undefined,
"key",
/* questionToken */ undefined,
f.createTypeReferenceNode("string")
);
const returnType = f.createTypeReferenceNode("Promise", [
f.createUnionTypeNode([
f.createTypeReferenceNode("string"),
f.createLiteralTypeNode(f.createNull()),
]),
]);
const getMethod = f.createMethodSignature(
/* modifiers */ undefined,
"get",
/* questionToken */ undefined,
/* typeParameters */ undefined,
[keyParameter],
returnType
);
const kvNamespace = f.createInterfaceDeclaration(
/* decorators */ undefined,
/* modifiers */ undefined,
"KVNamespace",
/* typeParameters */ undefined,
/* heritageClauses */ undefined,
[getMethod]
);
const file = ts.createSourceFile("file.ts", "", ts.ScriptTarget.ESNext);
const printer = ts.createPrinter();
const output = printer.printNode(ts.EmitHint.Unspecified, kvNamespace, file);
console.log(output); // interface KVNamespace { get(key: string): Promise<string | null>; }
Whilst the auto-generated types correctly describe the JavaScript interface of Workers runtime APIs, TypeScript provides additional features we can use to provide higher-fidelity types and improve developer ergonomics. Our system allows us to handwrite partial TypeScript “overrides” that get merged with the auto-generated types. This enables us to…
Add type parameters (generics) to types such as
ReadableStream
and avoidany
typed values.Specify the correspondence between input and output types with method overloads. For example,
KVNamespace#get()
should return astring
when thetype
argument istext
, butArrayBuffer
when it’sarrayBuffer
.Rename types to match TypeScript standards and reduce verbosity.
Fully-replace a type for more accurate declarations. For example, we replace
WebSocketPair
with aconst
declaration for better types withObject.values()
.Provide types for values that are internally untyped such as the
Request#cf
object.Hide internal types that aren’t usable in your workers.
Previously, these overrides were defined in separate TypeScript files to the C++ declarations they were overriding. This meant they often fell out-of-sync with the original declarations. In the new system, overrides are defined alongside the originals with C++ macros, meaning they can be reviewed alongside runtime implementation changes. See the README for workerd
’s JavaScript glue code for many more details and examples.
Try typing with workers-types today!
We encourage you to upgrade to the latest version of @cloudflare/workers-types
with npm install --save-dev @cloudflare/workers-types@latest
, and try out the new wrangler types
command. We’ll be publishing a new version of the types with each workerd
release. Let us know what you think on the Cloudflare Developers Discord, and please open a GitHub issue if you find any types that could be improved.