npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@raegen/tscheck

v0.2.0

Published

Type-check TypeScript 7 monorepos with project references — no emit, no build artifacts, attributed errors

Readme

tscheck

npm CI

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.

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 a warning: 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@^7

TypeScript 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 report

The 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 like tsc --pretty false: file(line,col): error TS1234: message, with paths relative to the current directory.
  • --pretty prints them exactly like tsc --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 prints 1 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-pretty

actions/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 --own checks only one package's own projects, which is faster in per-package CI jobs.
  • npx tscheck --format json > tscheck-report.json produces 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-references makes every project in the graph owned.
  • --own implies --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-references without --own to skip them in a full run. If --own leaves nothing to check, for example for a solution in a subfolder (configs/tsconfig.all.json referencing ../packages/*), tscheck prints a warning: … was not checked line for each skipped reference.
  • Some diagnostics point into files that belong to no project: lib files, node_modules declarations, or a file that is imported but matched by no project's include. These diagnostics are owned by the project whose check reported them. tscheck 0.1 left them unowned, so --own hid 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.json that includes all of src/ and references tsconfig.app.json. TypeScript checks such shared files only in the referenced project, so with --own tscheck 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:

  1. Locate TypeScript 7 and start one server process for the whole run. 7.0 uses typescript/unstable/async, 7.1 uses typescript/async.
  2. Resolve the reference graph. tscheck reads references from every tsconfig itself, because the API exposes neither references nor cycles. The server parses each config (files and options, extends included). All paths are realpath'd, so symlinked workspaces work.
  3. 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.ts outputs, no build order, no TS6305, and nothing is written to disk. Upstream sources are parsed once and shared by all projects.
  4. 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 --concurrency projects in flight.
  5. 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.
  6. 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> --noEmit byte 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 -b would 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 --verbose to 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 -b would not. A reference with extra lib or types entries can hide errors that tsc -b would report. TypeScript 7 enables strict by default and no longer includes @types packages unless they are listed in types. 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 -b then stops without checking anything (exit code 4). tscheck drops the back edge and checks every project. References marked "circular": true are accepted silently, as in tsc.

  • Emit-only option errors are not reported, exactly as with tsc --noEmit. These are TS5011 (rootDir must be set), TS5055 (Cannot write file … because it would overwrite input file), TS5056 and TS5096 (allowImportingTsExtensions without noEmit, which --noEmit satisfies).

  • A missing reference (TS6053) is worded like tsc -p (File '…/missing' not found.). tsc -b words 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 a warning: 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 -b with up-to-date .tsbuildinfo files 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 tscheck collects 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 set GOMEMLIMIT below 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. --concurrency limits CPU use, not peak memory, because all projects are opened in one snapshot. In the example, --concurrency 1 peaked 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 -p on 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`);
  • onProjectStart and onProjectEnd fire for projects of kind 'project' only. Solution configs are checked silently. Projects run in parallel and finish in any order.
  • onProjectEnd receives that project's diagnostics, attributed and deduplicated within the project. Two projects can report the same diagnostic, for example when they share files. result.diagnostics is 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 with formatDiagnostic(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 older typescript. 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 try GOGC=50. A slow run gets faster with --skip-declaration-diagnostics. See Performance.
  • context canceled above error: 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 --verbose and look for note: lines about option divergence (see Differences from tsc -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