npm package: @raegen/tscheck
Description: Lightweight type-checker for TypeScript monorepos with project references - no emit required
Author: raegen
Latest version: 0.1.7

# tscheck

Lightweight type-checker for TypeScript monorepos with project references - no emit required.

## Why?

TypeScript's `tsc --build` requires emitting files (at minimum `--emitDeclarationOnly`). This means:
- You need `outDir` configured
- Files are written to disk
- CI pipelines need to handle generated artifacts

`tscheck` solves this by building `.d.ts` declarations **in-memory only**, allowing you to type-check monorepos with project references without producing any output files.

## Warning

Bear in mind what this actually means. Because it's by no means magic. 
What tscheck does is a tradeoff:

<font style="color: green">＋ no emitted artifacts to manage and a slight speed improvement</font>

<font style="color: red">－ noticably higher memory usage</font>

So, if you're looking for a plug&play typechecking in a monorepo and you can afford the memory usage, this might be worth a try. Otherwise, just stick to stock.

## Installation

```bash
npm install -D @raegen/tscheck typescript
```

## CLI Usage

```bash
# Check the default tsconfig.json
tscheck

# Check a specific project
tscheck packages/app/tsconfig.json

# Only care about errors in THIS project, not its dependencies
tscheck packages/app/tsconfig.json --own

# Solution-style config: check each referenced project and report all errors
tscheck tsconfig.json

# Verbose: show which project is being checked and a summary at the end
tscheck tsconfig.json -v

# Skip references pointing outside the tsconfig's own directory
tscheck tsconfig.json --ignore-external-references
```

### Options

| Flag | Description |
|------|-------------|
| `-p, --project <path>` | Path to tsconfig.json (default: `./tsconfig.json`) |
| `--own-errors-only, --own` | Only report errors owned by the target project (see [Ownership](#ownership)); implies `--ignore-external-references` |
| `-r, --include-references` | Treat all projects in the graph as "owned" (report all errors regardless of origin) |
| `--ignore-external-references` | Skip project references that resolve outside the referencing tsconfig's directory |
| `--show-ownership` | Show which project owns each error |
| `-t, --timing` | Show per-project timing breakdown |
| `--pretty / --no-pretty` | Enable/disable colored output (default: enabled when stdout is a TTY) |
| `-v, --verbose` | Print each project as it is checked and a summary at the end |
| `--debug` | Show debug information about the project graph and diagnostic attribution |

### Exit Codes

| Code | Meaning |
|------|---------|
| `0` | No type errors (or no owned errors when `--own`) |
| `1` | Type errors found |
| `2` | Configuration or runtime error |

## Ownership

The `--own` flag controls which errors count toward the exit code and are displayed.

**For a plain (non-solution) tsconfig**, owned files are those matched by its `include`/`files` patterns. Errors in files that are only reachable via imports from dependencies — even if those dependencies are listed in `references` — are not owned.

**For a solution-style tsconfig** (`files: []` with only `references`), tscheck recurses through the solution chain. Any leaf tsconfig reachable by following only solution-style nodes is owned. The `references` inside those leaf configs (e.g. a `shared` library added by a sync tool) are dependency references — their errors are not owned from the solution's perspective.

**Example:**

```
monorepo/tsconfig.json          ← solution (files: [])
  packages/app/tsconfig.json    ← solution (files: [])
    tsconfig.app.json           ← leaf (owned from monorepo root)
    tsconfig.node.json          ← leaf (owned from monorepo root)
      packages/shared           ← dependency reference (NOT owned from app, IS owned from monorepo root)
```

Running `tscheck packages/app/tsconfig.json --own` will **not** report errors in `packages/shared`, because `shared` is only a dependency of the leaf configs, not part of the app's solution chain.

Running `tscheck monorepo/tsconfig.json --own` **will** report errors in `packages/shared`, because `shared` appears as a leaf in the monorepo's solution chain (it has files and is reachable from the root).

`--own` implies `--ignore-external-references` so that tsconfig references added by tools like the NX TypeScript sync generator (which may point to workspace peers outside the package directory) are silently skipped rather than causing spurious TS6305 errors.

## Programmatic API

### Simple API

```typescript
import { check } from '@raegen/tscheck';

const result = check('./tsconfig.json');

if (!result.success) {
  console.log(result.formattedDiagnostics);
  console.log(result.formattedSummary);
  process.exit(1);
}
```

`check()` accepts either a path string or an options object:

```typescript
import { check } from '@raegen/tscheck';

const result = check({
  project: './tsconfig.json',
  ownErrorsOnly: true,
  includeReferences: true,
  ignoreExternalReferences: true,
  colors: true,
});
```

### Low-level API

```typescript
import { checkProjects } from '@raegen/tscheck';

const result = checkProjects({
  project: './tsconfig.json',
  ownErrorsOnly: true,
  onProjectStart: (project, index, total) => {
    console.log(`[${index + 1}/${total}] ${project.configPath}`);
  },
  onDiagnostic: (attributed) => {
    if (attributed.isOwned) process.stdout.write('!');
  },
});

if (!result.success) {
  console.log(`Found ${result.ownedErrorCount} owned errors`);
  process.exit(1);
}
```

### CheckOptions

```typescript
interface CheckOptions {
  /** Path to root tsconfig.json */
  project: string;
  /** Only count/report errors owned by the target project (see Ownership section) */
  ownErrorsOnly?: boolean;
  /** Treat all projects in the graph as "owned" */
  includeReferences?: boolean;
  /**
   * Skip project references that resolve outside the referencing tsconfig's directory.
   * Implied when ownErrorsOnly is true.
   */
  ignoreExternalReferences?: boolean;
  /** Callback for each diagnostic as it's found */
  onDiagnostic?: (diagnostic: AttributedDiagnostic) => void;
  /** Callback when starting to check a project (only fires for projects with files) */
  onProjectStart?: (project: ProjectNode, index: number, total: number) => void;
  /** Callback when finished checking a project (only fires for projects with files) */
  onProjectEnd?: (project: ProjectNode, diagnostics: AttributedDiagnostic[], durationMs?: number) => void;
}
```

### CheckResult

```typescript
interface CheckResult {
  /** Whether type-checking passed with no errors */
  success: boolean;
  /** All diagnostics (errors and warnings) */
  diagnostics: ts.Diagnostic[];
  /** Diagnostics with project attribution */
  attributedDiagnostics: AttributedDiagnostic[];
  /** Number of projects checked (solution-style umbrella configs excluded) */
  projectCount: number;
  /** Total number of files checked */
  fileCount: number;
  /** Total number of errors */
  errorCount: number;
  /** Number of warnings */
  warningCount: number;
  /** Number of errors owned by the checked project(s) */
  ownedErrorCount: number;
  /** Number of errors from dependencies */
  dependencyErrorCount: number;
  /** Per-project timing information */
  projectTimings: ProjectTiming[];
  /** Total checking duration in milliseconds */
  totalDurationMs: number;
}
```

### AttributedDiagnostic

```typescript
interface AttributedDiagnostic {
  /** The underlying TypeScript diagnostic */
  diagnostic: ts.Diagnostic;
  /** The project that was being checked when this diagnostic was found */
  sourceProject: ProjectNode;
  /** The project that owns the file where the error is */
  owningProject: ProjectNode | null;
  /** Whether this error is owned by the target project (see Ownership section) */
  isOwned: boolean;
}
```

## How It Works

1. **Parse project graph** — Resolves `tsconfig.json` and all project references into a dependency graph, topologically sorted so dependencies come first.
2. **Virtual file system** — Creates an in-memory compiler host that captures `.d.ts` output from each project.
3. **Type-check** — Runs TypeScript's semantic analysis on each project in order, writing declarations to memory rather than disk.
4. **Cross-project resolution** — Later projects resolve imports from earlier ones via the in-memory `.d.ts` files, exactly as `tsc --build` would, but without touching the filesystem.
5. **Diagnostic attribution** — Every diagnostic is tagged with its owning project, enabling `--own` and `--show-ownership` to work correctly across the graph.

## License

MIT