Coverage Reports
scriptc coverage tells you how much of a program compiles before you build it. Use it when you bring an existing Node.js program to scriptc, add a dependency, or want CI to catch unsupported code early. It analyzes the program without producing an executable, counts the statements that compile, and groups anything unsupported by diagnostic code with a location and a hint.
Run a report
Pass the program's entry file:
$ cat hello.ts
const who: string = process.argv.length > 2 ? process.argv[2] : "world";
console.log(`hello, ${who}`);
$ scriptc coverage hello.ts
statements analyzed 2
compile statically 2 (100%)
fully static — all analyzed statements compile to native code.Every statement in this program compiles, so scriptc build will succeed. The counts describe this program only, not JavaScript or Node.js support in general.
Reading the report
When a program has unsupported operations, the report adds a group for each root cause. A group shows the diagnostic code, the reason, the first few sites in source order, and a hint. A generic declaration that fails for several type arguments appears once.
| Field | Meaning |
|---|---|
statements analyzed | Executable statements in application code, excluding dependency implementation code. |
compile statically | Statements that compile to native instructions. |
in dynamic tier | Of those, the statements in functions that operate on values typed only at runtime, such as any values and untyped JavaScript. Shown when there are any. See Dynamic tier. |
blockers | Unsupported operations with counts, diagnostic codes, reasons, locations, and hints. |
runs at startup | Deferred operations in a JavaScript module's top-level code, which runs when the module loads: at startup for a module the program imports. |
deferred to runtime | Other deferred operations in JavaScript code. Each throws only if it runs. |
in unreached code | Blockers in code the entry never reaches. A build never compiles that code, so these cannot fail it. |
differs from Node | Sites that compile but can behave differently from Node.js. See Node divergences. |
trusts unverified types | Checked conversions whose value is typed by something no build can verify. See Type claims. |
npm package compilation | Native package compilation outcomes and refusal reasons. |
Blockers in unreached code and deferred operations in JavaScript packages are listed separately because they don't fail a build. A deferred operation throws a runtime error if it runs, so a successful build doesn't mean every API of a package works; see unsupported code inside a package.
If a function's signature can't compile, the statements in its body count as not compiling, so the percentage reflects how much of the program compiles. The report still lists the diagnostics of those statements; a use of a parameter whose type can't compile reports SC2004 back to the signature.
Large programs
A full report lowers the entry path, then lowers the rest of the program to list its diagnostics. On a large program the second pass can need more memory than the analysis has.
Analyze one entrypoint per run, and pass --coverage=reached to skip the second pass:
$ scriptc coverage hello.ts --coverage=reached
statements analyzed 2
compile statically 2 (100%)
fully static — all analyzed statements compile to native code.
note unreached code was not analyzed; the totals count reached statements only and are not comparable with a full reportThe report then has no "in unreached code" group, and its totals count only the statements the entry path reaches. A function whose signature can't compile still lists its body's diagnostics when the entry path reaches it. The note marks the totals as not comparable with a full report.
--coverage=reached can't be combined with --explain-dynamic, which needs the unreached code.
Dynamic tier
Code that operates on values whose types are known only at runtime, such as any values, the result of an untyped JSON.parse, or untyped JavaScript, compiles into dynamic-tier functions. They are native code too, but they check types and look up properties at runtime, so they are slower than code with static types. The report's in dynamic tier line counts their statements.
--explain-dynamic lists those functions and the operation that makes each one dynamic. It also lists the units an unsupported construct would move to the dynamic tier: the binding, field, class, function, or module that has no static representation for the construct, with the diagnostic behind each one. These are the constructs the dynamic tier is planned to compile; today they remain blockers in TypeScript and deferred runtime errors in JavaScript. Add a name, such as --explain-dynamic=Point, to show only the matching units. With --print=diagnostics, the document gains a tiers object with the same information.
The report also explains runtime-optional storage. A binding or parameter whose value comes from a read that can miss, such as an array element or a record key, keeps storage that holds undefined, while the checker types every read with the declared type. TypeScript's narrowing at those reads therefore does not reach the storage, and lowering extracts the value through a checked narrowing. For each such slot the report names the expression that made it optional, whether the slot is a binding, a pattern binding or a parameter, and the reads where the checker's type is narrower than the storage. With --print=diagnostics, the tiers object carries the same slots.
--tier=static-only keeps every unsupported construct a blocker instead of moving it to the dynamic tier; code that already operates on runtime-typed values compiles as before. Today auto, the default, behaves the same way.
Untyped code that likely runs often
When functions of the program's own files use untyped values and likely run often, scriptc build and coverage report them as an SC6101 warning. For the Benchmarks Game n-body program in plain JavaScript:
$ scriptc build nbody.js -o nbody
warning SC6101: 3 functions that likely run often use untyped values, which runs slower than typed code:
advance (nbody.js:22): untyped JavaScript file, in a loop
energy (nbody.js:40): untyped JavaScript file, in a loop
offsetMomentum (nbody.js:12): untyped JavaScript file, in a loop
hint: add types to compile these statically; see scriptc coverage --explain-dynamic
The program compiled. Adding types to these functions lets scriptc compile them to faster code.Without a profile, a dynamic-tier function counts as likely to run often when a dynamic operation runs on every iteration of one of its loops (a for loop's initializer and a for...of loop's iterable run once), when a loop calls it, or when it calls itself. A function is named only when its source says why its values are untyped: a value typed any (for a for...of variable, the value it iterates), an untyped JSON.parse result, or an untyped JavaScript file. Dynamic code that runs once, such as setup code that reads an any configuration, is not reported, and neither are functions that are dynamic for an operation with no static lowering, which more types would not change.
The warning names at most five functions, most likely to run often first, and gives the total. A function expression is named by its own name, else by the variable, property or class field it is assigned to, else as anonymous function. Dynamic-tier code in npm packages is summarized on one line with each package's function count. A build that comes from the cache repeats the warning. --no-dynamic-tier-warning leaves it out, and --fail-on=dynamic fails a coverage run on it.
Dynamic slots
A declared type that has no native representation does not fail the build. The field, parameter, return value, variable or expression that holds it becomes a checked dynamic slot: the parts of the type that compile keep their native layout, and the failing parts hold checked-dynamic values. Coverage lists the slots under the statement counts, and scriptc build prints one SC6104 warning per slot.
$ cat plugins.ts
type Hook = (() => void) | AbortSignal | Map<string, (x: number) => string>;
interface Plugin {
name: string;
hook: Hook;
}
const plugins: Plugin[] = [{ name: "log", hook: () => console.log("hook ran") }];
for (const p of plugins) {
const hook = p.hook;
if (typeof hook === "function") hook();
console.log(p.name);
}
$ scriptc coverage plugins.ts
statements analyzed 6
compile statically 6 (100%)
in dynamic tier 1 (operate on values typed only at runtime; --explain-dynamic lists them)
dynamic slots 1 (declared types with no static lowering, held as checked-dynamic values; SC6104)
module global 'plugins' (declared 'Plugin[]') (dynamic pieces) plugins.ts:8:7
fully static — all analyzed statements compile to native code.Here Plugin keeps its native name field, and only hook is checked-dynamic. Reading a dynamic value into typed code checks it against the type that reads it, so data that contradicts the declaration still throws a TypeError. Slots with the most dynamic operations are listed first. --fail-on=dynamic-slots fails a coverage run when any slot exists, and --tier=static-only reports the original errors instead.
Type claims
scriptc trusts declared types and checks them where an untyped value enters typed code. A value that contradicts its type throws a TypeError at that conversion, where Node.js would run on. Some of those types are claims no build can verify: an assertion over parsed data, the type a variable gives an any value, and the declarations of a package whose values your program receives. When a claim is wrong, the build succeeds and the program throws the first time the conversion runs. Coverage lists these conversions before anything runs:
$ cat claims.ts
interface User {
id: number;
name: string;
}
const user = JSON.parse('{"id":"41","name":"Ada"}') as User;
const settings: any = JSON.parse('{"retries":3}');
const retries: number = settings.retries;
console.log(user.name, user.id + 1, retries * 2);
$ scriptc coverage claims.ts
statements analyzed 4
compile statically 4 (100%)
in dynamic tier 1 (operate on values typed only at runtime; --explain-dynamic lists them)
fully static — all analyzed statements compile to native code.
trusts unverified types 2 checked conversions on 2 claims (compiles; a value that contradicts its claimed type throws a TypeError at the conversion)
×1 parsed data held in 'settings', read as number
at claims.ts:8:25
fix: validate the parsed data before giving it this type (a schema, or checks on an unknown value), or declare the type the data really has
×1 parsed data the program reads as { id: number; name: string }
at claims.ts:6:14
fix: validate the parsed data before giving it this type (a schema, or checks on an unknown value), or declare the type the data really has
$ scriptc build claims.ts -o claims >/dev/null
$ ./claims
Uncaught TypeError: expected number at $.id, got stringThe second claim is the wrong one here: the data holds id as a string. The report can't know which claims are wrong, only where they are.
Conversions are grouped by what makes the claim, so one wrong declaration appears once with every conversion that trusts it:
| Group | What makes the claim | What makes it safe |
|---|---|---|
| A package declaration | A .d.ts file, an @types/ package or JSDoc that types a value your code receives from the package. The row names the package, the declaration, and its file and line. | The scriptc types patch command the row prints. See Fix a package's types. |
| A function a Node island calls | The package's declaration of the parameter your function is passed as. The island calls the function with its own values, and each argument is checked against your function's parameter types. | The same command, with the parameter's selector. |
| A package compiled from its JavaScript | The package's own JSDoc and the types scriptc infers for its untyped code. All conversions inside the package's files are one row. | Patch the declaration that is wrong, or run the package in a Node island with --node-island=<package>. |
| Parsed data | The type your program gives the result of JSON.parse. | Validate the data before you give it the type. |
| An untyped value | The type a variable, parameter or return value gives a value read from a binding declared any or unknown. The row names the binding. | Declare the binding's real type, or narrow the value before typed code reads it. |
| An assertion | A type assertion over a value the program holds dynamically, such as table() as Catalog where table() returns a Record<string, object>. The row names what the value is read from. | Check the value before asserting its type, or build it under a declared type so the checker verifies it. |
Rows are ordered by the number of conversions, after two kinds of rows that come first, and before the rows for packages compiled from their JavaScript, whose counts are of the package's own files.
A Node island copies data to your program and adapts functions, but an object the island made never converts to a type only compiled code can make, such as a Date, a RegExp, or an http.Server. A conversion like that throws every time it runs, unless the value is one your program passed into the island, so those rows come first and say so.
Next come the rows with a conversion whose failure your program would not see: one inside a try whose catch neither reads the error nor throws. This function is meant to skip malformed lines. Compiled, it also skips every well-formed line that lacks a field Entry declares, where Node.js keeps the entry, and nothing reports it:
$ cat entries.ts
interface Usage {
input: number;
total: number;
}
interface Entry {
type: "message";
usage: Usage;
}
function parse(line: string): Entry | null {
try {
return JSON.parse(line) as Entry;
} catch {
return null;
}
}
const lines = [
'{"type":"message","usage":{"input":1,"total":2}}',
'{"type":"message","usage":{"input":1}}',
"not json",
];
for (const line of lines) {
const entry = parse(line);
console.log(entry === null ? "skipped" : `${entry.type} ${entry.usage.input} ${String(entry.usage.total)}`);
}
$ scriptc coverage entries.ts | tail -n 5
trusts unverified types 1 checked conversion on 1 claim (compiles; a value that contradicts its claimed type throws a TypeError at the conversion)
×1 parsed data the program reads as { type: string; usage: { input: number; total: number } }
at entries.ts:12:12
unseen if it fails: entries.ts:12:12 is in a try whose handler ignores the error, where Node.js keeps the value
fix: validate the parsed data before giving it this type (a schema, or checks on an unknown value), or declare the type the data really has
$ scriptc build entries.ts -o entries >/dev/null
$ ./entries
message 1 2
skipped
skipped
$ node entries.ts
message 1 2
message 1 undefined
skippedOnly a conversion written inside the try block is marked, not one in a function the block calls.
A conversion that a kind test of the same value guards is not a claim and is not listed: a read inside if (typeof value === "string"), in the matching arm of a conditional, or after if (typeof value !== "string") return. A test elsewhere in the function guards nothing, and neither does a test of a variable the function assigns again. Neither is reading back a value that your TypeScript source declares with a type and scriptc holds dynamically, such as a Response from fetch. In JavaScript files nothing declares a type, so every such read is listed.
This program reads two members of a logger that the debug package creates. The package runs in a Node island, and its types come from @types/debug:
$ cat logger.ts
import createDebug from "debug";
const log = createDebug("app:main");
log("hidden unless DEBUG is set");
console.log(log.enabled, log.namespace);
$ scriptc coverage logger.ts | tail -n 7
trusts unverified types 2 checked conversions on 2 claims (compiles; a value that contradicts its claimed type throws a TypeError at the conversion)
×1 [email protected] declares debug.Debugger.enabled as boolean (@types/debug/index.d.ts:44): a Node island value
at logger.ts:5:13
fix: if the declaration is wrong, patch it: scriptc types patch debug debug.Debugger.enabled '<type>' (a type that starts with | widens the declared one, as in '| undefined')
×1 [email protected] declares debug.Debugger.namespace as string (@types/debug/index.d.ts:46): a Node island value
at logger.ts:5:26
fix: if the declaration is wrong, patch it: scriptc types patch debug debug.Debugger.namespace '<type>' (a type that starts with | widens the declared one, as in '| undefined')
$ scriptc build logger.ts -o logger >/dev/null 2>&1
$ ./logger
Uncaught TypeError: expected boolean at $, got undefined
[email protected] declares debug.Debugger.enabled as boolean (@types/debug/index.d.ts:44)
if that declaration is wrong, patch it: scriptc types patch debug debug.Debugger.enabled '| undefined'The first declaration is wrong: enabled is undefined when the DEBUG environment variable isn't set. The error names the declaration the report listed, with the type that would admit the value. That command fixes it, and the program then prints what it prints under Node.js:
$ scriptc types patch debug debug.Debugger.enabled "| undefined"
@types/debug 4.4.3 (node_modules/@types/debug)
index.d.ts:44 boolean -> boolean | undefined
wrote package.json scriptc.typePatches["debug@^4.4.3"].declarations["debug.Debugger.enabled"]
$ scriptc build logger.ts -o logger >/dev/null 2>&1
$ ./logger
undefined app:mainA declaration with type parameters has no patch command, because its type is the one your program instantiates it with, and neither has a member of a mapped or inferred type. The row says so and points to replacing the declaration file instead.
The report describes the program a build compiles, including which packages run in Node islands. If scriptc build reports a Node island for a package that coverage compiled, pass the same packages to coverage with --node-island=<package> to see the claims of that build. --fail-on=type-claims fails a coverage run when the program has any claim.
Diagnostic codes
Coverage and scriptc build report the same diagnostic codes. Common ones include:
SC2020: a declared standard-library or Node.js API has no supported compiler implementation.SC2012: this API form cannot compile natively.SC2013: a package cannot compile natively; inspect the package-specific reason and the dependency guide.SC1100: a value whose type is only known at runtime, such asunknown, is used where scriptc needs a specific type. Narrow or cast the value as the diagnostic suggests.
Node divergences
Some code compiles but can behave differently than under Node.js. Coverage reports the cases it can detect as SC6xxx warnings, and scriptc build prints the same warnings after a successful build. They never fail a build, but --fail-on=divergences can fail a CI job on them.
SC6001: a conversion to a record, array, or class type with another layout copies a value something else can still reach, and the program writes a field or element of either type, compares values of the converted type by identity, or passes either to code scriptc cannot follow.SC6002: values of one record type are built in key orders scriptc cannot track per value, so the type keeps its declaration order, an object literal's key order differs from it, and the program observes the order throughJSON.stringify,Object.keys,Object.values,Object.entries,for...in, or console output.SC6003:localeComparecompares a string literal outside the built-in Latin-script collation.SC6004: a call passes a class instance to a function that casts it to an unrelated class, or a value typed by an interface that extendsErroris handed to a slot typed by anErrorclass.SC6007: an object or array made in a Node island is copied where Node.js keeps one object: it is converted to an array type of the program, or to a record type that declares a string-keyed dictionary or a library object (an assertion, a typed binding of ananyvalue, a typed parameter of a callback the island calls), or it is stored untyped in a member, an element, a reassigned binding or a container and given a type after it is read back; see Objects converted from Node islands.SC6012: a JavaScript statement that doesn't compile sits in thetryblock of atry/catchin the same function. It throws when it runs, so thecatchclause runs even where the statement would have completed under Node.js, and the compiled program doesn't report it at run time.SC6013: a statement writes a member that the program added to the libraryPromisetype, such asPromise<T> & { status?: string }. A compiled promise holds no members of its own, so the write throws aTypeErrorwhen it runs on a promise the program made. Keep the state beside the promise, in a wrapper record or in aWeakMapkeyed by the promise.SC6014: the program observes the key order of a record type whose objects create their keys in orders scriptc doesn't follow per object, such as a spread of a value whose key order isn't known, orObject.assignadding two or more optional properties from a value converted at run time. The warning names the type once, by its keys, and the objects the program builds can still list their keys as under Node.js; see Record identity and property order.SC6015: a TypeScript statement doesn't compile and its refusal waits for the statement to run. It throws an error that names the refusal when it runs, where it would have completed under Node.js; a program that never reaches the statement runs as it does under Node.js. The same code names a statement of a generic function that takes a value as the one object type its constraint names while the call binds objects of several layouts: the statement throws aTypeErrorfor a value of another layout.
Gating builds and CI
By default, scriptc coverage exits with status 0 whenever the program type-checks, so you can use the report to track progress. To fail a CI job instead, add --fail-on:
$ scriptc coverage src/main.ts --fail-on=blockers--fail-on=blockers exits with status 1 when the program has blockers that would fail scriptc build. Blockers in unreached code and deferred operations don't count. --fail-on=divergences also fails on Node divergences, --fail-on=dynamic also fails on the SC6101 warning for untyped code that likely runs often, --fail-on=dynamic-slots also fails on dynamic slots, and --fail-on=type-claims also fails on type claims. A program that doesn't type-check always exits with status 1.
Coverage caches its result. Running it again on an unchanged program, or after edits that only touch comments, reuses the previous verdict. Set SCRIPTC_NO_CACHE=1 to turn the cache off.
Machine-readable diagnostics
For editors, CI annotations, and other tools, scriptc coverage --print=diagnostics and scriptc build --print=diagnostics write one JSON document to stdout instead of the human-readable report. The exit status is unchanged. The document looks like this:
{
"schema": "scriptc-diagnostics",
"schemaVersion": 1,
"compilerVersion": "0.3.0",
"command": "coverage",
"entry": "/work/app/src/main.ts",
"success": false,
"phase": "compile",
"stats": {
"statementsTotal": 13,
"statementsStatic": 12,
"statementsDynamic": 0,
"statementsFailed": 1,
"functionsSkipped": 0,
"percentStatic": 92
},
"groups": [
{
"id": "SC1031-c2cc8425",
"code": "SC1031",
"category": "unsupported",
"severity": "error",
"scope": "reached",
"message": "object destructuring of non-record values (the source is array-typed)",
"hint": "...",
"count": 1,
"primary": { "file": "/work/app/src/main.ts", "line": 13, "column": 7 },
"sites": [{ "file": "/work/app/src/main.ts", "line": 13, "column": 7 }]
}
],
"diagnostics": [
{
"code": "SC1031",
"category": "unsupported",
"severity": "error",
"scope": "reached",
"file": "/work/app/src/main.ts",
"line": 13,
"column": 7,
"endLine": 13,
"endColumn": 28,
"start": 352,
"end": 373,
"message": "object destructuring of non-record values (the source is array-typed) are not supported yet",
"hint": "...",
"group": "SC1031-c2cc8425"
}
]
}| Field | Meaning |
|---|---|
schemaVersion | Incremented when a field is renamed or removed or its meaning changes. New optional fields, categories, and codes keep the version. |
success | Coverage: no blockers on the entry path (and, with --fail-on=divergences, no divergences). Build: an artifact was produced, named by artifact. |
phase | preflight when type checking, configuration, or the module graph stopped the command; compile when blockers remain; native when native code generation or linking failed; otherwise complete. |
stats | Coverage only, absent after a preflight failure: the report's statement counts. statementsStatic counts every statement that compiles to native code; statementsDynamic is the part of those in dynamic-tier functions. |
category | invalid-source (the program has a TypeScript or JSON error), unsupported (valid code scriptc cannot compile yet), divergence (compiles, but can behave differently from Node.js), performance (compiles and behaves as under Node.js, but runs slower than typed code would), environment (project configuration, scriptc's type world, toolchain, or target), or internal (a compiler bug). |
severity | error, or warning for divergences. |
scope | preflight, reached (fails a build), unreached (code the entry never reaches), or deferred (a JavaScript statement that compiles to a throw of its diagnostic, which happens only if it executes). |
line, column | 1-based line and UTF-16 column; start and end are 0-based UTF-16 offsets. |
group | The id of the root-cause group the diagnostic belongs to. A group's primary site is its first in source order. |
typeClaims | Coverage only, present when the program has type claims: one object per row of the report, in the report's order. kind is island, declared, package, parsed or untyped; count and sites give the conversions; claimed lists the types they check for; fix is the report's fix line. A row about a package declaration has declaration with its package, version, name, file and line (at), declaredType, and the selector to pass to scriptc types patch when it has one. A package row lists those for each of the package's own declarations under declarations. nativeOnly counts the conversions that throw whenever they run, swallowed lists the sites inside a try that ignores the error, callbacks the functions a Node island calls, and asserted the type assertions. |
startup | true on a deferred diagnostic in a module's top-level code, which runs when the module loads. Absent otherwise. |
Type errors
Coverage needs a program that type-checks. If type checking fails, the report lists the type errors instead of statement counts; fix them first.
scriptc type-checks with its own settings: strict null checks, the ES2025 library, and Node.js types (its bundled declarations when the project has no @types/node). An error those settings cause, rather than the project's own tsc configuration, carries a hint naming the cause, such as a library member newer than ES2025 or a global of another runtime, and machine-readable output reports it in the environment category.
External host type declarations
When your code will run inside a host application that provides its own modules, those modules aren't in node_modules. Use --external-types to give coverage their declarations so it can analyze the rest of your code:
$ scriptc coverage src/core.ts --external-types @native-sdk/core=types/native-sdk-core.d.tsThe option is repeatable. Relative declaration paths resolve from the current working directory. Supported declaration extensions are .d.ts, .d.mts, and .d.cts. Relative declaration imports and re-exports are included, so the mapped file can be an index.d.ts barrel. Project-owned code using these types participates in coverage; type-only imports add no runtime boundary.
The mapping supplies types for analysis, not a runtime implementation. Value imports and uses remain SC1010 external-host blockers while analysis counts the rest of the application. This option is available only for coverage; builds require a module implementation or runtime integration.