Isolated Declarations¶
An opt-in throughput mode, one value for the whole build:
--//ts:declarations=oxc. The default is --//ts:declarations=tsgo.
Earlier versions required the opt-in mode. The tsgo action builds a complete
type program per target to type-check, and emits the declarations from it;
oxc is the opt-in path for the extra throughput.
What It Means¶
TypeScript normally writes math.d.ts by running type inference: it has to know
what add() returns. The emit therefore needs the whole program, meaning this
file, its imports, their .d.ts, and so on.
With isolated declarations, each file's .d.ts is derived from that file's
syntax alone, because every export carries an explicit type. No program, no
dependency types, no inference.
What It Buys¶
Both modes cache the same way: change math.ts without changing its exported
types and the emitted .d.ts is byte-identical, so Bazel skips every downstream
target.
The mode buys pipelining. Oxc emits a .d.ts without a type program, so a
dependent waits for a per-file transform rather than for TsgoDeclare, tsgo's
declaration emit over the whole upstream program; the type check is a
validation action nothing blocks on in either mode. On a deep dependency chain
that shortens the critical path substantially; shallower graphs narrow the gap,
deeper ones widen it. tools/bench_declarations.sh 20 50 3 measures it on
1,000 annotated files across 20 packages in one linear chain
(Cost of Each Mode); run it on your
own graph.
The Requirement¶
// Rejected under --//ts:declarations=oxc — the return type is inferred
export function add(a: number, b: number) {
return a + b;
}
// Accepted
export function add(a: number, b: number): number {
return a + b;
}
It applies to every exported function, arrow function, and variable. Oxc reports violations itself and fails the build:
× Isolated declarations error(s): TS9007: Function must have an explicit
│ return type annotation with --isolatedDeclarations.
The build fails because a syntactic emitter can only widen an un-annotated
export: an object of five RegExps becomes {}, a RegExp becomes unknown,
and the target still builds. Oxc did not check and tsgo saw valid TypeScript,
so nothing reports it and the error surfaces later in a consumer, against the
wrong file:
parseDomain.ts(95,51): error TS2339: Property 'idPreview' does not exist on type '{}'.
parseDomain.ts(96,41): error TS18046: 'UUID_PATTERN' is of type 'unknown'.
The mode has no partial version. The flag is one value for the build, so either
every package's exports are annotated and the build uses oxc, or it stays on
the default. A rule that wants one subtree under the other value writes a
Starlark transition on the flag; tests/flags.bzl is the ruleset's own.
What the ESLint Rule Covers¶
| Export pattern | Flagged when |
|---|---|
export function foo() {} |
No : ReturnType annotation |
export const fn = () => ... |
No return type on arrow or binding annotation |
export const x = someExpression |
No : Type annotation on binding |
export function foo(a) {} |
A parameter has no : Type annotation |
export class Foo { bar = 1 } |
A property or method of the class is unannotated |
export default function() {} |
No : ReturnType annotation |
export default { a: 1 } |
Expression default export with no type context |
Where the type is readable straight off the AST (a literal, a uniform array
literal, a single-return body) the report carries an auto-fix, so
eslint --fix annotates it. Everything else is reported with a suggestion to
annotate by hand; the rule never guesses an object literal's shape.
The rule does NOT flag export type, export interface, export enum,
re-exports (export { x } from '...'), or ambient declarations, so a clean lint
run does not guarantee a clean oxc build. Oxc is the authority.
Options¶
ignoreDefaultExports (default false) skips every export default form:
rules: {
'isolated-declarations/require-explicit-types': [
'error',
{ ignoreDefaultExports: true },
],
}
Migration¶
Every build starts on --//ts:declarations=tsgo and builds. Move over when
throughput matters and every package's exports are annotated.
Step 1. Install the ESLint plugin that reports missing annotations.
The plugin is not yet published to npm. Build it from the eslint-plugin/
directory in the rules_typescript repository and install the tarball:
# From the rules_typescript checkout:
cd path/to/rules_typescript/eslint-plugin
npm install
npm run build # tsup -> dist/
npm pack
# produces rules_typescript-eslint-plugin-isolated-declarations-<version>.tgz,
# where <version> is the `version` in eslint-plugin/package.json
# In your project:
npm install --save-dev \
path/to/rules_typescript/eslint-plugin/rules_typescript-eslint-plugin-isolated-declarations-0.2.0.tgz \
@typescript-eslint/parser \
eslint
Configure it in eslint.config.js:
import isolatedDeclarations from '@rules_typescript/eslint-plugin-isolated-declarations';
export default [
{
plugins: { 'isolated-declarations': isolatedDeclarations },
rules: { 'isolated-declarations/require-explicit-types': 'error' },
},
];
The bundled config, isolatedDeclarations.configs.recommended, does the same.
Step 2. Run the linter over the repository:
Step 3. Add the missing explicit types. Annotate accurately: a : any or an
over-wide annotation degrades the declaration as much as the widening this mode
prevents.
Step 4. Switch the build over in .bazelrc and build:
If anything was missed, Oxc fails the build and names the file and line.