docs/reference/ts-style.md
This document provides a checklist for reviewing JavaScript and TypeScript code in the workerd project. It covers style guidelines, common pitfalls, and critical patterns to watch for when ensuring code quality.
../../tools/base.eslint.config.mjs../../tools/base.tsconfig.json../../.prettierrc.jsonMany conventions below are enforced by ESLint and the TypeScript compiler. The checklist focuses
on what automated tools miss and on verifying that enforced rules are not being bypassed with
eslint-disable comments.
For ../../src/node/ specifics, see ../../src/node/AGENTS.md and ../../src/node/README.md.
For ../../src/cloudflare/ specifics, see ../../src/cloudflare/AGENTS.md and ../../src/cloudflare/README.md.
The project uses strict TypeScript with several settings stricter than typical:
exactOptionalPropertyTypes: true — optional properties cannot be explicitly set to
undefined unless the type includes | undefined. E.g., { x?: string } does not accept
{ x: undefined }.noUncheckedIndexedAccess: true — indexed access on arrays and records returns T | undefined.
Code must narrow or assert before using the value.verbatimModuleSyntax: true — type-only imports must use import type { X } or inline
import { type X, Y }. A bare import { X } where X is only used as a type is a compile error.erasableSyntaxOnly: true — no TypeScript enums, no namespace declarations, no parameter
properties. Only syntax that erases cleanly to JavaScript is permitted.noImplicitReturns: true, noUnusedLocals: true, noUnusedParameters: true —
unused variables are prefixed with _ to signal intent.Import specifiers follow a tiered module system:
| Specifier prefix | Visibility | Purpose |
|---|---|---|
node:* | Public | Standard Node.js module API |
cloudflare:* | Public | Cloudflare product APIs |
node-internal:* | Private | Internal Node.js implementation (not user-facing) |
cloudflare-internal:* | Private | Cloudflare runtime-provided internals (not user-facing) |
node:*, cloudflare:*) must not import from internal specifiers of the
other tier. src/node/ code must not import cloudflare-internal:* and vice versa.*-internal:*) are not available to user code. Do not expose them in
public module exports..d.ts files without a matching .ts file declare the shape of C++ JSG modules imported
via internal specifiers.Public modules (src/node/*.ts, src/cloudflare/*.ts) may use a dual-export pattern:
export { Foo, Bar, type Baz } from 'internal-module';
export default { Foo, Bar };
Both named exports and a default export object are required for compatibility with different import styles.
Internal modules export directly:
export class Foo { ... }
export function bar(): void { ... }
Never re-export namespace imports as default. The ESLint rule
workerd/no-export-default-of-import-star (in src/node/) enforces:
// BAD
import * as X from 'module';
export default X;
// GOOD — explicit named re-exports
export { foo, bar } from 'module';
export default { foo, bar };
Always use #private syntax for private members, not the private keyword:
// GOOD
class Foo {
#bar: string;
#doSomething(): void { ... }
}
// BAD — flagged by ESLint no-restricted-syntax
class Foo {
private bar: string;
private doSomething(): void { ... }
}
Exceptions exist in older code with eslint-disable comments (e.g., when # syntax conflicts
with implements interface constraints). New code should not add new exceptions without
justification.
Never use the public keyword. Members without an accessibility modifier are public by default.
// GOOD
class Foo {
bar: string;
doSomething(): void { ... }
}
// BAD — flagged by ESLint
class Foo {
public bar: string;
public doSomething(): void { ... }
}
../../src/node/:Always use Node.js-style error classes from node-internal:internal_errors:
import { ERR_METHOD_NOT_IMPLEMENTED } from 'node-internal:internal_errors';
throw new ERR_METHOD_NOT_IMPLEMENTED('setEngine');
Never throw bare Error objects when a matching ERR_* class exists.
../../src/cloudflare/:Define product-specific error classes extending Error:
export class InferenceUpstreamError extends Error {
constructor(message: string, name = 'InferenceUpstreamError') {
super(message);
this.name = name;
}
}
Runtime compat flags are available via the global Cloudflare.compatibilityFlags:
if (!Cloudflare.compatibilityFlags.some_flag) {
// legacy behavior
}
Always gate any behavioral change that could break existing workers behind a compat flag.
See src/workerd/io/compatibility-date.capnp for the flag schema.
Tests are plain .js files (not .ts) using node:assert. No test frameworks.
Named export pattern (preferred for multiple tests per file):
export const someFeatureTest = {
test() {
assert.strictEqual(actual, expected);
},
};
export const anotherFeatureTest = {
test() {
assert.strictEqual(actual, expected);
},
};
Default export pattern (for handler-based tests):
export default {
async fetch(request, env, ctx) { ... },
async test(ctrl, env, ctx) { ... },
};
Each test .js file is paired with a .wd-test Cap'n Proto config. See the wd-test-format
skill for config details.
Mock services (*-mock.js) export a default handler for simulating external services:
export default {
async fetch(request, env, ctx) { ... },
};
just format before submittingWhen reviewing JS/TS code in workerd, check for each of these items.
.ts and .js file must begin
with the project copyright/license header using the current year. Expected format:
// Copyright (c) <current-year> Cloudflare, Inc.
// Licensed under the Apache 2.0 license found in the LICENSE file or at:
// https://opensource.org/licenses/Apache-2.0
import type for type-only imports. With verbatimModuleSyntax, type-only imports
must use import type { X } or import { type X, Y }. The compiler catches this, but verify
it in review since CI may not always run on every file..ts files must have explicit
return types (enforced by ESLint). Check that new functions comply and that eslint-disable is
not being used to bypass this.# syntax for private members instead of the private keyword.public keyword on class members.!). Prefer proper narrowing or ?? defaultValue.
Flag any eslint-disable for @typescript-eslint/no-non-null-assertion.erasableSyntaxOnly. Use as const
objects for enum-like patterns.compatibility-date.capnp and is
checked at runtime.src/node/ and src/cloudflare/
must provide both named exports and a default export object.eslint-disable without justification. Every eslint-disable comment should name a
specific rule and explain why the override is necessary. Blanket eslint-disable (no rule
name) is never acceptable.@ts-expect-error without explanation. Must include a comment explaining why the type
system cannot express the correct type. Prefer fixing the types over suppressing the error.require(). Banned. Use ESM import syntax and dynamic import() if necessary.eslint-disable. If the suppressed lint no longer fires, the disable comment
should be removed. Review for stale suppressions.