@raegen/tscheck
v0.2.0
Published
Type-check TypeScript 7 monorepos with project references — no emit, no build artifacts, attributed errors
Maintainers
Readme
tscheck
Type-check a TypeScript 7 monorepo with project references without building it. tscheck writes no
.d.ts, .js or .tsbuildinfo files, and it tells you which project owns each error.
$ npx tscheck packages/app/tsconfig.app.json --show-ownership --no-pretty
packages/app/src/dependency-import.ts(1,44): error TS2307: Cannot find module 'shared' or its corresponding type declarations.
packages/shared/src/bad.ts(1,14): error TS2322: Type 'number' is not assignable to type 'string'. [from: packages/shared]
Found 2 errors in 2 files (1 own, 1 in dependencies).
Done in 485ms.- Why
- Requirements
- Installation
- CLI usage
- Using tscheck in CI
- TypeScript 6 and 7 side by side
- Ownership
- How it works
- Differences from
tsc -b - Performance
- Programmatic API
- JSON report
- Troubleshooting
Upgrading from 0.1? Read MIGRATION.md. The release notes are in CHANGELOG.md.
Why
TypeScript 7 (tsc) has no command that type-checks a project-reference graph without writing files. These are the
results on a small monorepo where packages/app references packages/shared, with TypeScript 7.0.2:
| Command | What happens |
|---|---|
| tsc -b --noEmit | Every reference fails with error TS6310: Referenced project '…/packages/shared' may not disable emit. The .tsbuildinfo files are written anyway. |
| tsc -p packages/app --noEmit | error TS6305: Output file '…/shared/dist/index.d.ts' has not been built from source file '…/shared/src/index.ts'. This happens until the reference has been built. Composite projects still write their .tsbuildinfo. |
| tsc -b --noCheck | Exits 0 even when the code has type errors. It also writes dist/ and .tsbuildinfo. |
| tsc -b | Works, but it emits JavaScript, declarations and .tsbuildinfo for every project. CI then has to manage those build artifacts. |
tscheck checks the whole reference graph and writes nothing to disk. Each project gets the same diagnostics that
tsc -p <project> --noEmit would report if its references were already built. tscheck prints them in tsc's exact
format, reports each error only once, and attributes it to the project that owns the file. That makes it possible to
fail a run only on errors in the package you are working on (--own).
Requirements
- TypeScript 7. tscheck is tested with 7.0.x (7.0.2 and later). Support for 7.1 is experimental and tested
against nightly builds. The tested range is
>=7.0.2 <7.2.0. Other 7.x versions still run, with awarning: tscheck 0.2.0 has not been tested with TypeScript …line. - Node.js 20.11 or later.
- TypeScript 5.x or 6.x? Use tscheck 0.1.x:
npm install -D @raegen/[email protected]. To keep TypeScript 6 for your build and check with 7, see side by side.
The typescript peer dependency is optional and deliberately loose (>=5.0.0, plus 7.1 prereleases), so that npm
installs the side-by-side setup, where typescript is 5.x or 6.x and TypeScript 7 sits under an alias, and nightly
builds. tscheck checks the version itself when it runs: anything other than TypeScript 7 fails with a message that
explains what to install or pass to --typescript.
Installation
npm install -D @raegen/tscheck typescript@^7TypeScript 7 ships its compiler as a native binary in a platform-specific optional dependency. Do not install with
--omit=optional.
CLI usage
tscheck # the nearest tsconfig.json (searched upwards like tsc) and its references
tscheck packages/app # a directory containing tsconfig.json
tscheck -p packages/app/tsconfig.app.json # a specific config file
tscheck packages/app --own # only the app's own projects and their errors
tscheck --show-ownership # tag errors in dependencies with [from: <project>]
tscheck --format json > tscheck-report.json # machine-readable reportThe project to check is a tsconfig file, or a directory that contains tsconfig.json. Without one, tscheck uses the
nearest tsconfig.json in the current directory or a parent directory, like tsc does. tscheck checks that config and
every project it references, directly or transitively.
Options
| Option | Description |
|---|---|
| [project], -p, --project <path> | tsconfig file or directory to check. Give it only once, either as a positional argument or with -p. Put it after -- if it starts with -. |
| --own, --own-errors-only | Check only the owned projects (see Ownership) and report only their errors. Implies --ignore-external-references. |
| -r, --include-references | Treat every project in the reference graph as owned. |
| --ignore-external-references | Do not follow references that point outside the referencing tsconfig's directory. |
| --show-ownership | Append [from: <project>] to the first line of every error in a dependency. |
| --format <text\|json> | text (default): diagnostics in tsc's format and a summary on stdout, notes on stderr. json: one JSON report on stdout, also when the check cannot run. |
| --pretty [true\|false], --no-pretty | Colors and code frames, like tsc --pretty. If several are given, the last one wins. Default: on when stdout is a terminal (see Environment). |
| --skip-declaration-diagnostics | Skip the declaration emit checks (TS4xxx, TS9xxx, …) that tsc runs for projects with declaration or composite enabled. In benchmarks this saved roughly 15–30% of the wall time. |
| --concurrency <n> | Maximum number of projects checked in parallel. Default: the number of CPU cores. |
| --typescript <path\|package> | The TypeScript 7 to use: a package directory, or a package name resolved from the current directory. See side by side. |
| -t, --timing | Print per-project and per-phase timings instead of the Done in line. A project's time runs from its first request to its last response. Projects are checked in parallel, so their times overlap and include waiting for the shared server; use --concurrency 1 to compare projects. |
| -v, --verbose | Print progress ([2/5] packages/app) and all notes on stderr, plus the number of files and projects checked. |
| --debug | Print the project graph and where every error came from on stderr. |
| -h, --help | Show the help. |
| --version | Show the version. |
An unknown option is an error (exit code 2). tscheck 0.1 silently ignored unknown options.
Exit codes
| Code | Meaning |
|---|---|
| 0 | No errors. With --own: no errors in owned projects. |
| 1 | Errors were found, or an owned project could not be checked (see the warning: lines). |
| 2 | Invalid arguments, tsconfig or TypeScript not found, the TypeScript server failed, or the output could not be written. |
| 130 | Interrupted (SIGINT, Ctrl-C). |
| 143 | Terminated (SIGTERM). |
Output
In text mode, stdout gets the diagnostics, the summary (Found 4 errors in 2 files. or No errors found.), and then
Done in … or the timing table. stderr gets the progress lines, notes, warnings and --debug output. Fatal errors go
to stderr as error: <message>.
--no-pretty(the default when stdout is not a terminal) prints diagnostics exactly liketsc --pretty false:file(line,col): error TS1234: message, with paths relative to the current directory.--prettyprints them exactly liketsc --pretty: colors, code frames and tsc's error summary table.- The summary adds an ownership breakdown when dependencies have errors, for example
Found 2 errors in 2 files (1 own, 1 in dependencies).With--own, it says how many dependency errors were hidden. warning:lines are always shown. They appear when a project could not be checked, or when the TypeScript version is outside the tested range.note:lines (for example option divergence) only appear with--verbose. Without it, tscheck prints1 note hidden; run with --verbose to see them.
Environment
| Variable | Effect |
|---|---|
| NO_COLOR | If non-empty, pretty output is off unless --pretty is given. |
| FORCE_COLOR | If non-empty (any value, even 0), pretty output is on unless NO_COLOR is set or --no-pretty is given. This matches tsc 7. |
| GOGC, GOMEMLIMIT | Passed through to the TypeScript server, which is a Go program. See Performance. |
Using tscheck in CI
GitHub Actions:
name: Type check
on: [push, pull_request]
jobs:
typecheck:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 22
cache: npm
- run: npm ci
- run: npx tscheck --no-prettyactions/setup-node registers a problem matcher for tsc's output format. tscheck's --no-pretty output is tsc's own
format, so every error is shown as an annotation on the pull request diff, with no extra setup. Pretty output is off
anyway when stdout is not a terminal. Passing --no-pretty keeps it off if FORCE_COLOR is set on the runner.
Other options:
npx tscheck packages/app --ownchecks only one package's own projects, which is faster in per-package CI jobs.npx tscheck --format json > tscheck-report.jsonproduces a report for custom tooling. The exit code is the same as in text mode.- On small runners, see the memory notes in Performance.
TypeScript 6 and 7 side by side
You can keep TypeScript 5 or 6 as typescript for your build and editor, and install TypeScript 7 under an alias for
tscheck:
{
"devDependencies": {
"@raegen/tscheck": "^0.2.0",
"typescript": "^6.0.0",
"typescript7": "npm:typescript@^7.0.2"
}
}npx tscheck --typescript typescript7--typescript takes a package name, which is resolved from the current directory, or a path to the package directory
(--typescript node_modules/typescript7). The programmatic option is typescript.
Without --typescript, tscheck resolves typescript from the root tsconfig's directory, then from the current
directory, then from tscheck's own location. If that finds TypeScript older than 7, tscheck looks for
@typescript/native in the same places. So an alias named "@typescript/native": "npm:typescript@^7.0.2" is found
without any flag.
Both packages declare a tsc binary, so node_modules/.bin/tsc may point at either one. Scripts that need a
particular compiler can call it by path, for example node node_modules/typescript/bin/tsc.
Ownership
Ownership decides which errors are "own" errors and which are errors "in dependencies". With --own, only own errors
are reported and only they decide the exit code. Without it, every error is reported and fails the run, and the
summary shows the breakdown.
For a plain (non-solution) tsconfig, the owned files are the ones matched by its include/files patterns.
Errors in files of the projects it references are dependency errors.
For a solution-style tsconfig ("files": [] with only references), tscheck follows the solution chain. Every
tsconfig reached by following only solution-style configs is owned. So is the first project with files on each path
(a leaf). The references inside those leaf configs are dependency references, and their errors are not owned from
the solution's point of view. An example is a shared library added by a sync tool.
Example:
tsconfig.json ← solution (files: [])
packages/shared/tsconfig.json ← leaf (owned from the root)
packages/app/tsconfig.json ← solution (files: [])
tsconfig.app.json ← leaf (owned from the root and from packages/app)
tsconfig.node.json ← leaf (owned from the root and from packages/app)
../shared ← dependency reference (NOT owned from packages/app, IS owned from the root)tscheck packages/app --own does not report errors in packages/shared. shared is only a dependency of the
leaf configs, not part of the app's solution chain, so with --own tscheck does not check it at all. The app's
projects still resolve imports from shared's sources.
tscheck --own at the root does report errors in packages/shared. shared is a leaf in the root's solution
chain, because it has files and is reachable from the root through solution configs only.
tscheck packages/app/tsconfig.app.json --show-ownership (without --own) checks shared as well and reports its
errors as dependency errors:
$ npx tscheck packages/app/tsconfig.app.json --show-ownership --no-pretty
packages/app/src/dependency-import.ts(1,44): error TS2307: Cannot find module 'shared' or its corresponding type declarations.
packages/shared/src/bad.ts(1,14): error TS2322: Type 'number' is not assignable to type 'string'. [from: packages/shared]
Found 2 errors in 2 files (1 own, 1 in dependencies).
Done in 485ms.With --own, the same command reports only the TS2307: shared is a dependency, so it is not checked.
More rules:
-r/--include-referencesmakes every project in the graph owned.--ownimplies--ignore-external-references. A reference is external when it points outside the referencing tsconfig's directory, for example a workspace peer added by the Nx TypeScript sync generator. External projects are never owned or checked. TypeScript still reads their sources to resolve imports. Pass--ignore-external-referenceswithout--ownto skip them in a full run. If--ownleaves nothing to check, for example for a solution in a subfolder (configs/tsconfig.all.jsonreferencing../packages/*), tscheck prints awarning: … was not checkedline for each skipped reference.- Some diagnostics point into files that belong to no project: lib files,
node_modulesdeclarations, or a file that is imported but matched by no project'sinclude. These diagnostics are owned by the project whose check reported them. tscheck 0.1 left them unowned, so--ownhid them. - A file that an owned project lists is always owned, even when a dependency lists it too. A common case is a
tsconfig.spec.jsonthat includes all ofsrc/and referencestsconfig.app.json. TypeScript checks such shared files only in the referenced project, so with--owntscheck also checks a dependency that lists files of an owned project. - With
--own, the summary may say(N errors in dependencies ignored). These are errors that an owned project's own check reported in a dependency's files. They are rare, because each project's sources are checked by that project only.
How it works
TypeScript 7 is a native (Go) program and has no in-process compiler API. Instead, it offers an IPC API: a
tsc-compatible server that loads tsconfigs the way an editor does. tscheck is built on that API:
- Locate TypeScript 7 and start one server process for the whole run. 7.0 uses
typescript/unstable/async, 7.1 usestypescript/async. - Resolve the reference graph. tscheck reads
referencesfrom every tsconfig itself, because the API exposes neither references nor cycles. The server parses each config (files and options,extendsincluded). All paths are realpath'd, so symlinked workspaces work. - Open a source-redirect snapshot. All projects to check are opened in one snapshot. Like an editor, the server
resolves every project reference to the referenced project's source files. No
.d.tsoutputs, no build order, no TS6305, and nothing is written to disk. Upstream sources are parsed once and shared by all projects. - Check projects in parallel. The server handles requests for one project one at a time, but runs requests for
different projects in parallel. So tscheck keeps one request stream per project, with
--concurrencyprojects in flight. - Collect diagnostics like tsc. Each project's diagnostics are collected in tsc's stages: config, syntactic,
program and global, semantic, then declaration. A later stage only runs if the earlier ones found nothing, as in
tsc -p <project> --noEmit. The server does not semantically check upstream source files inside a downstream program, so each file's type errors come from its own project only. - Attribute, sort and deduplicate every diagnostic exactly like tsc 7, then format it exactly like tsc 7, plain
or pretty. Parity tests compare the output with
tsc -p <project> --noEmitbyte for byte.
docs/ARCHITECTURE.md explains the modules and the TypeScript 7 behaviours behind each design decision.
Differences from tsc -b
Editor semantics for references. tscheck checks a referenced project's sources with the downstream project's compiler options.
tsc -bwould instead read declarations that were emitted with the reference's own options. When the options that affect typing differ (strict,strictNullChecks,strictFunctionTypes,exactOptionalPropertyTypes,noUncheckedIndexedAccess,lib,types), tscheck adds a note. Run with--verboseto see it:note: app/tsconfig.json checks the sources of its reference lib/tsconfig.json with its own compiler options, which differ in: strict, strictNullChecks, strictFunctionTypes. It is stricter (strict, strictNullChecks, strictFunctionTypes), so it may report errors that tsc -b would not.A stricter downstream project can report errors that
tsc -bwould not. A reference with extralibortypesentries can hide errors thattsc -bwould report. TypeScript 7 enablesstrictby default and no longer includes@typespackages unless they are listed intypes. The comparison takes both into account.Each project is checked on its own merits. A syntax error in an upstream project is reported once, by that project, and does not hide the downstream project's type errors. The same goes for errors in an upstream tsconfig, such as TS6306 for a reference to a non-composite project or TS6053 for a missing one.
Cycles are reported, not fatal. A reference cycle produces TS6202 with
tsc -b's exact message.tsc -bthen stops without checking anything (exit code 4). tscheck drops the back edge and checks every project. References marked"circular": trueare accepted silently, as in tsc.Emit-only option errors are not reported, exactly as with
tsc --noEmit. These are TS5011 (rootDirmust be set), TS5055 (Cannot write file … because it would overwrite input file), TS5056 and TS5096 (allowImportingTsExtensionswithoutnoEmit, which--noEmitsatisfies).A missing reference (TS6053) is worded like
tsc -p(File '…/missing' not found.).tsc -bwords it as…/missing/tsconfig.json.Duplicates are printed once. When several projects report the same diagnostic, for example two configs that share
src/, it is printed only once.Malformed references on TypeScript 7.0. The 7.0.2 server crashes on a reference such as
{ "path": 5 }. tscheck reports TS5024 itself, skips every project that reaches such a config, prints awarning:for each one, and exits 1. TypeScript 7.1 reports TS5024 itself and checks everything.No incremental mode. Every run is a full check. A warm, no-op
tsc -bwith up-to-date.tsbuildinfofiles is faster on reruns.
Performance
These numbers are indicative and depend on the machine. They come from a synthetic monorepo of 25 projects × 150 files (3,775 files, about 387,000 lines) on an 8-core Apple M2 with 16 GB:
| Command | Wall time | Peak memory |
|---|---|---|
| tscheck 0.2 (TypeScript 7.0.2) | about 2 s (1.9–2.6 s) | about 2 GB |
| tscheck 0.2 --skip-declaration-diagnostics | about 1.6–1.8 s | about 2 GB |
| TypeScript 7 tsc -b, cold (writes all outputs) | 2.8–3.9 s | about 1.9 GB |
| TypeScript 7 tsc -b, warm no-op | 0.3 s | 360 MB |
| tscheck 0.1.7 (TypeScript 5.9) | 22–34 s | 0.8–1.2 GB |
| TypeScript 5.9 tsc -b, cold | 24–35 s | 0.9–1.45 GB |
- Memory. The server keeps every opened program in memory, about as much as a cold
tsc -b. tscheck passes the environment through to the server, which is a Go program, so its garbage collector can be tuned.GOGC=50 npx tscheckcollects more often. In our measurements it lowered the peak by up to about 15% and cost up to about 25% more time; the effect varies by repository. Do not setGOMEMLIMITbelow the live heap (about 1.7 GB in the example above). The garbage collector then runs constantly and the check becomes about 3× slower. - Concurrency.
--concurrencylimits CPU use, not peak memory, because all projects are opened in one snapshot. In the example,--concurrency 1peaked at 2.0 GB versus 2.1 GB with the default. To lower memory on a small runner, check packages separately (tscheck packages/x --own). - One huge project. Each project is checked by one checker in the server. So a repository dominated by a single
very large project can be up to about 2× slower than
tsc -pon that project, which spreads its files over several checkers.
Programmatic API
The package is ESM-only; CommonJS code can require() it on Node versions that support require() of ES modules
(20.19 and 22.12 or later), and import() it everywhere. Every function that checks is async and writes nothing to
disk. Type errors are returned as data. Problems that prevent a check from running throw a
TscheckError. All paths in results are absolute, normalized to forward slashes, and realpath'd. The type
declarations need neither @types/node nor the DOM lib: signal accepts any AbortSignal (its type is the
structural AbortSignalLike).
check(options?): check and format
import { check } from '@raegen/tscheck';
const result = await check({ project: 'packages/app', ownErrorsOnly: true, showOwnership: true });
process.stdout.write(result.formattedDiagnostics + result.formattedSummary + result.formattedTimings);
process.stderr.write(result.formattedNotices);
process.exitCode = result.success ? 0 : 1;check() takes a project path string, or CheckOptions plus the ReportOptions of formatReport.
It returns the CheckResult with the four sections of formatReport() as extra strings: formattedDiagnostics,
formattedSummary, formattedTimings (with timing) and formattedNotices (warnings such as an untested TypeScript
version, plus notes and totals with verbose). Like the CLI, write the first three to stdout and the notices to
stderr. pretty defaults to the CLI's rule (defaultPretty(process.env, process.stdout.isTTY === true): NO_COLOR,
then FORCE_COLOR, then whether stdout is a terminal).
checkProjects(options?): the full result
import fs from 'node:fs';
import { checkProjects, formatDiagnostics, projectLabel } from '@raegen/tscheck';
// TypeScript reports realpath'd file names, so use the realpath of the working directory as the display base.
// `.native` also expands Windows 8.3 short names (C:\Users\RUNNER~1), like tscheck does.
const cwd = fs.realpathSync.native(process.cwd());
const controller = new AbortController();
process.once('SIGINT', () => controller.abort());
const result = await checkProjects({
project: 'tsconfig.json',
cwd,
concurrency: 4,
signal: controller.signal,
onProjectStart(project, index, total) {
process.stderr.write(`[${index + 1}/${total}] ${projectLabel(project, cwd)}\n`);
},
onProjectEnd(project, diagnostics, durationMs) {
const label = `${projectLabel(project, cwd)} (${Math.round(durationMs)} ms)`;
process.stderr.write(`${label}: ${diagnostics.length} diagnostics\n`);
},
});
process.stdout.write(formatDiagnostics(result.diagnostics.map((d) => d.diagnostic), { cwd }));
process.stdout.write(`${result.errorCount} errors, ${result.ownedErrorCount} own, in ${result.projectCount} projects\n`);onProjectStartandonProjectEndfire for projects of kind'project'only. Solution configs are checked silently. Projects run in parallel and finish in any order.onProjectEndreceives that project's diagnostics, attributed and deduplicated within the project. Two projects can report the same diagnostic, for example when they share files.result.diagnosticsis the final list: sorted and deduplicated like tsc, with graph diagnostics (TS6202, TS5024 on 7.0) included. Print that list if you need tsc's order.- To print a project's diagnostics as soon as it finishes, pass them to
formatDiagnostics(list, options), which terminates every diagnostic like tsc's reporter. If you format single diagnostics withformatDiagnostic(d), add a'\n'after each one yourself, as tsc's reporter does. In pretty mode, a diagnostic with a code frame already ends with a newline: that is tsc's blank line between diagnostics. - When the signal aborts, tscheck shuts the TypeScript server down and the call rejects with
TscheckError('ABORTED'). A run never returns partial results.
createChecker(options?) and getProjectGraph(options?)
import { createChecker, getProjectGraph } from '@raegen/tscheck';
const checker = createChecker({ project: 'tsconfig.json', ownErrorsOnly: true });
const graph = await checker.getProjectGraph();
for (const project of graph.projects) {
console.log(project.kind, project.configPath, project.external ? '(external)' : '');
}
const result = await checker.check({ declarationDiagnostics: false });
console.log(result.success, result.checkedProjects.length);
const fullGraph = await getProjectGraph({ project: 'tsconfig.json' });
console.log(fullGraph.root.kind, fullGraph.projects.length);createChecker() binds options for repeated checks. checker.check(overrides) runs checkProjects() with the bound
options, and overrides take precedence; an override that is undefined keeps the bound value.
checker.getProjectGraph() returns the graph that check() would use. The
checker is stateless: every call starts its own server and reads the files on disk again.
getProjectGraph() resolves the reference graph without checking anything. The TypeScript server still starts,
because it parses the tsconfigs. It accepts ownErrorsOnly for the ignoreExternalReferences default it implies. graph.projects is in topological order: dependencies first, root last.
Formatting
import { checkProjects, defaultPretty, formatReport } from '@raegen/tscheck';
const result = await checkProjects({ project: 'tsconfig.json' });
const pretty = defaultPretty(process.env, process.stdout.isTTY === true); // tsc's NO_COLOR / FORCE_COLOR / TTY rule
const report = formatReport(result, { pretty, showOwnership: true, timing: true });
process.stdout.write(report.diagnostics + report.summary + report.timings);
process.stderr.write(report.notices);
process.exitCode = result.success ? 0 : 1;formatReport(result, options) renders the same sections the CLI prints. Each section is '' or newline-terminated
text.
| Section | Contents |
|---|---|
| diagnostics | Diagnostics in tsc 7's exact format. Only owned ones when the run used ownErrorsOnly. |
| summary | The result line, or tsc's summary block when pretty, then Done in … (unless timing). |
| timings | The per-project and per-phase timing table when timing is set, else ''. |
| notices | warning: lines, note: lines (with verbose), or the N notes hidden hint. With verbose also the totals line. |
ReportOptions: pretty, cwd (display base, default result.cwd), sourceText, showOwnership, timing,
verbose. sourceText is a SourceTextProvider that supplies file text for line/column and code frames; the default
is a fresh SourceTextCache, which reads from disk and decodes like TypeScript. Pass one new SourceTextCache() to
several formatting calls to read each file once, or your own provider for in-memory text.
Lower-level formatters, all byte-compatible with tsc 7:
| Function | Purpose |
|---|---|
| formatDiagnostics(diagnostics, options?) | Many diagnostics, joined and newline-terminated, like tsc's reporter. |
| formatDiagnostic(diagnostic, options?) | One diagnostic, without the reporter's trailing newline. |
| suffix option of both | Text appended to a diagnostic's first line (tscheck's [from: …] tags): a string, or a function of the diagnostic returning one (or undefined). |
| formatTscSummary(diagnostics, options?) | tsc's pretty error summary (Found 2 errors in 2 files. plus the table). |
| flattenMessage(diagnostic) | The message with its chain, indented like tsc. |
| formatDuration(ms) | 42ms or 1.50s. |
| defaultPretty(env, stdoutIsTTY) | The default of pretty, decided like tsc 7: NO_COLOR, then FORCE_COLOR, then the terminal. |
| SourceTextCache | The default SourceTextProvider: reads and decodes each file once. |
| projectLabel(project, cwd) | Short project name: its directory for tsconfig.json, otherwise the config path. |
| toJsonReport(result) / toJsonErrorReport(error) | The JSON report objects that --format json prints. |
Errors
import { checkProjects, isTscheckError } from '@raegen/tscheck';
try {
const result = await checkProjects({ project: 'packages/app', typescript: 'typescript7' });
process.exitCode = result.success ? 0 : 1;
} catch (error) {
if (!isTscheckError(error)) throw error;
console.error(`${error.code}: ${error.message}`);
process.exitCode = 2;
}TscheckError has a code:
| Code | When |
|---|---|
| CONFIG_NOT_FOUND | The tsconfig (or directory) to check does not exist, or no tsconfig.json was found upwards from cwd. |
| TYPESCRIPT_NOT_FOUND | No typescript package could be resolved, or the typescript option did not resolve. |
| UNSUPPORTED_TYPESCRIPT | The resolved TypeScript is not version 7, or tscheck does not recognize its API layout. |
| ENGINE_FAILED | The TypeScript server failed to start, crashed, or exited unexpectedly. |
| INVALID_OPTION | An option has an invalid value, for example concurrency: 0 or a cwd that does not exist. |
| ABORTED | The signal aborted. cause holds the signal's reason. |
isTscheckError() also works when several copies of tscheck are installed, where instanceof would fail.
Types
Excerpts from dist/types.d.ts. The JSDoc in the package has the full details.
interface CheckOptions {
project?: string; // tsconfig file or directory; default: nearest tsconfig.json from cwd upwards
cwd?: string; // default: process.cwd()
ownErrorsOnly?: boolean; // check owned projects only; only owned errors fail the run
includeReferences?: boolean; // every project in the graph is owned
ignoreExternalReferences?: boolean; // default: ownErrorsOnly
declarationDiagnostics?: boolean; // default: true
concurrency?: number; // default: os.availableParallelism()
typescript?: string; // package directory, or package name resolved from cwd
signal?: AbortSignalLike; // any AbortSignal
onProjectStart?: (project: ProjectNode, index: number, total: number) => void;
onProjectEnd?: (project: ProjectNode, diagnostics: AttributedDiagnostic[], durationMs: number) => void;
}
interface CheckResult {
success: boolean; // no (owned) errors, and every owned project was checked
options: EffectiveOptions; // the options after defaults and implications
cwd: string; // realpath'd working directory (the display base)
graph: ProjectGraph;
checkedProjects: ProjectNode[]; // topological order, solutions included
ownedProjects: ProjectNode[]; // topological order
diagnostics: AttributedDiagnostic[];// sorted and deduplicated like tsc 7
notices: Notice[]; // 'semantic-divergence' | 'not-checked' | 'untested-typescript'
projectCount: number; // checked projects of kind 'project'
fileCount: number; // unique root files of those projects
errorCount: number;
warningCount: number;
ownedErrorCount: number;
dependencyErrorCount: number;
projectTimings: ProjectTiming[]; // { project, durationMs }
timings: PhaseTimings; // { graphMs, startupMs, checkMs, totalMs }
typescriptVersion: string;
}
interface AttributedDiagnostic {
diagnostic: Diagnostic;
sourceProject: ProjectNode; // the project whose check reported it
owningProject: ProjectNode; // the project owning the file (falls back to sourceProject)
isOwned: boolean;
}
interface Diagnostic { // plain data, independent of TypeScript's own types
file?: string;
start?: number; // UTF-16 offset, as TypeScript reports it
length?: number;
line?: number; // 1-based, present when file and start are
column?: number;
endLine?: number;
endColumn?: number;
code: number;
category: 'error' | 'warning' | 'suggestion' | 'message';
message: string; // top-level message; the chain is in `next`
next?: DiagnosticMessageChain[];
relatedInformation?: Diagnostic[];
}
interface ProjectNode {
configPath: string; // realpath'd tsconfig path: the node's identity
kind: 'solution' | 'project'; // 'solution' = no files and at least one reference
files: string[]; // root files as TypeScript parsed them
references: ProjectNode[]; // cycle back edges removed
external: boolean; // reached only through a skipped external reference
}
interface ProjectGraph {
root: ProjectNode;
projects: ProjectNode[]; // every node, dependencies first, root last
byConfigPath: ReadonlyMap<string, ProjectNode>;
}The package also exports TSCHECK_VERSION and the types EffectiveOptions, Notice, NoticeKind, PhaseTimings,
ProjectTiming, ProjectKind, DiagnosticCategory, DiagnosticMessageChain, FormatOptions, ReportOptions,
FormattedReport, FormattedCheckResult, Checker, TscheckErrorCode, and the JSON report types.
JSON report
tscheck --format json prints exactly one line of JSON on stdout, and nothing else. toJsonReport(result) returns the
same object. The schema is versioned: version is 1, and it changes only when the schema changes incompatibly.
| Field | Contents |
|---|---|
| version, tscheckVersion, typescriptVersion | Schema version (1) and the tool versions. |
| success | Same as the exit code: true means 0. |
| cwd, root | The realpath'd working directory and the root tsconfig. |
| options | ownErrorsOnly, includeReferences, ignoreExternalReferences, declarationDiagnostics as used. |
| counts | errors, warnings, ownedErrors, dependencyErrors, projects, files. |
| projects[] | Every graph node in topological order: configPath, kind, external, checked, owned, files (count), references (config paths), durationMs (checked projects only; wall-clock, overlapping between projects checked in parallel, see --timing). |
| diagnostics[] | All diagnostics, owned or not, in tsc order: file, line, column, endLine, endColumn, start, length, code, category, message (chain flattened like tsc), owned, project (reporting config), owningProject, relatedInformation[]. Location fields are omitted when TypeScript gives none. |
| notices[] | kind, message, plus project, reference and options when they apply. |
| timings | graphMs, startupMs, checkMs, totalMs. |
All paths are absolute. For example (shortened):
{
"version": 1, "tscheckVersion": "0.2.0", "typescriptVersion": "7.0.2", "success": false,
"cwd": "/repo", "root": "/repo/packages/app/tsconfig.app.json",
"options": { "ownErrorsOnly": false, "includeReferences": false, "ignoreExternalReferences": false, "declarationDiagnostics": true },
"counts": { "errors": 1, "warnings": 0, "ownedErrors": 1, "dependencyErrors": 0, "projects": 2, "files": 3 },
"projects": [
{ "configPath": "/repo/packages/shared/tsconfig.json", "kind": "project", "external": false, "checked": true, "owned": false, "files": 1, "references": [], "durationMs": 157.4 },
{ "configPath": "/repo/packages/app/tsconfig.app.json", "kind": "project", "external": false, "checked": true, "owned": true, "files": 2, "references": ["/repo/packages/shared/tsconfig.json"], "durationMs": 154.5 }
],
"diagnostics": [
{ "file": "/repo/packages/app/src/dependency-import.ts", "line": 1, "column": 44, "endLine": 1, "endColumn": 52, "start": 43, "length": 8,
"code": 2307, "category": "error", "message": "Cannot find module 'shared' or its corresponding type declarations.",
"owned": true, "project": "/repo/packages/app/tsconfig.app.json", "owningProject": "/repo/packages/app/tsconfig.app.json" }
],
"notices": [],
"timings": { "graphMs": 39.5, "startupMs": 22.3, "checkMs": 157.7, "totalMs": 222.6 }
}When the check cannot run, the report is
{"version":1,"tscheckVersion":"0.2.0","error":{"code":"CONFIG_NOT_FOUND","message":"…"}} and the exit code is 2.
error.code is one of the TscheckError codes, or INTERNAL_ERROR for an unexpected failure (a bug in
tscheck).
Usage errors, such as an unknown option, are always printed as text on stderr: tscheck cannot know the output format
until the arguments have been parsed.
Troubleshooting
tscheck 0.2 requires TypeScript 7 (found 6.x …). tscheck resolved an oldertypescript. Install TypeScript 7, or point tscheck at one with--typescript(see side by side). For TypeScript 5 or 6 only, use@raegen/[email protected].Cannot start the TypeScript 7.0.2 server: Unable to resolve @typescript/typescript-<platform>.(ENGINE_FAILED). TypeScript 7's native binary is missing. Usually optional dependencies were not installed (--omit=optional, or a lockfile created on another platform without them). Reinstall with optional dependencies.- Symlinked workspaces (pnpm, npm/yarn workspaces, a symlinked checkout). tscheck realpaths the working directory, every tsconfig and every file, so imports between workspace packages resolve the same way as with tsc. Paths in the output are relative to the realpath of the current directory.
warning: … was not checked: … whose malformed reference crashes the TypeScript 7.0 server.A tsconfig has a reference such as{ "path": 5 }. tscheck reports TS5024 at that reference and exits 1, because the projects that depend on it could not be checked. Fix the reference, or use TypeScript 7.1, which handles it.- Out of memory on a small CI runner. Check packages separately with
--own, or tryGOGC=50. A slow run gets faster with--skip-declaration-diagnostics. See Performance. context canceledaboveerror: The operation was aborted.after Ctrl-C. The TypeScript server prints this line itself when the terminal sends it the interrupt as well. It is harmless.- Results differ from
tsc -b. Run with--verboseand look fornote:lines about option divergence (see Differences fromtsc -b). - Which projects were checked, and who owns an error? Run with
--debug. It prints every node of the graph (kind, owned or dependency, checked or not, file count, references) and, for each diagnostic, which project reported it and which project it is attributed to.
Authors
License
MIT
