angular-typechecker
v0.2.0
Published
Nx executor that runs the complete Angular compiler type-check (TypeScript + template type-check + extended NG8xxx diagnostics), no emit, decoupled from build and test.
Downloads
1,050
Maintainers
Readme
angular-typechecker
The complete Angular compiler type-check as a cacheable Nx target that does not build or emit.
angular-typechecker runs the full Angular compiler diagnostic set over a project and reports what it finds. It is built for the loops the editor does not cover: CI, pre-commit, and AI coding agents that need a static check on demand.
- Runs Angular's full diagnostic set (TypeScript, template type-checking, and
extended NG8xxx) in one pass, not just
tsc. - Emits nothing and builds nothing: no bundler, no test runner, no output files.
- Covers applications and every library kind (local/non-buildable, buildable, publishable), and checks the spec tsconfig in the same run.
- Runs as a cacheable Nx target, so unchanged projects are skipped on re-runs.
- Prints deterministic, ANSI-free output and exits non-zero on any error, so CI jobs and agents can gate on pass/fail directly.
It is a type-checker, not a build, a linter, or a test runner, and it does not replace your editor's Angular Language Service. It is the headless check you run everywhere the editor is not.
Contents
- What it does
- Requirements
- Installation
- Quick start
- Executor options
- Partial coverage
- Output
- Exit codes
- Continuous integration
- Programmatic API
- How it compares
- Storybook
- Limitations
- Contributing
- License
What it does
For each project, angular-typechecker walks the solution tsconfig.json down to
its in-project leaves (the app or lib tsconfig and tsconfig.spec.json) and
runs the Angular compiler with no emit. It gathers diagnostics from every phase
(option, syntactic, semantic, template, and extended NG8xxx) in one unconditional
pass, so a template or NG8xxx problem still surfaces even when a TypeScript error
sits in the same file. A bare ngc --noEmit stops at the first failing phase and
can bury the rest.
Requirements
| Tool | Supported version |
| --------------------------------------- | ------------------------------------- |
| Nx | 23.x |
| Angular (@angular/compiler-cli, peer) | 22.x stable (^22.0.0) |
| TypeScript (peer) | >=6.0.0 <6.1.0 |
| Node | ^22.22.3 \|\| ^24.15.0 \|\| ^26.0.0 |
@nx/devkit ships as a pinned dependency, so you never declare it yourself. You
provide @angular/compiler-cli and typescript, the versions your workspace
already uses.
Note: the ^22.0.0 peer range excludes Angular 22 pre-releases (-next /
-rc) by semver rules. To run the plugin on a 22.x pre-release, install with
--legacy-peer-deps, which relaxes peer resolution for the whole install, so
use it sparingly. The TypeScript window is narrow on purpose: Angular 22 supports
only TypeScript 6.0.x. The range may widen later; widening is non-breaking under
0.x semver.
Installation
nx add angular-typecheckernx add installs the package and runs its init generator, which seeds a
cacheable angular-typechecker:typecheck entry into nx.json targetDefaults.
Prefer plain npm? Install the package and run init yourself:
npm install --save-dev angular-typechecker
nx g angular-typechecker:initIn a pnpm workspace, add it to the workspace root and run init the same way:
pnpm add -Dw angular-typechecker
nx g angular-typechecker:initThis is nx add, not the Angular CLI's ng add, and there is no Angular-CLI
installer.
Quick start
Wire a project with the configuration generator, then run the target:
nx g angular-typechecker:configuration my-app
nx typecheck my-appThe generator adds a single typecheck target pointed at the project's solution
tsconfig.json. Because the engine walks that tsconfig's leaves, the spec
tsconfig is checked in the same run, so you never add a second target. Two flags
matter:
--tsConfig <path>points the target at a different tsconfig. It defaults to the solutiontsconfig.jsonand falls back totsconfig.app.json/tsconfig.lib.jsonwhen the project has no solution tsconfig with project references.--targetName <name>names the target something other thantypecheck.
Re-running the generator is safe: it will not overwrite a target of the same name that is not ours.
Wiring a project by hand
To skip the generator, add the target yourself. Point it at the solution
tsconfig.json and reference the published executor id
angular-typechecker:typecheck:
{
"targets": {
"typecheck": {
"executor": "angular-typechecker:typecheck",
"options": {
"tsConfig": "apps/my-app/tsconfig.json",
},
},
},
}Then add the cacheable targetDefaults entry to nx.json (this is what init
seeds). Its inputs are what make the whole-program check cache correctly: they
cover non-buildable dependency sources via ^default and buildable dependency
outputs via dependentTasksOutputFiles:
{
"targetDefaults": {
"angular-typechecker:typecheck": {
"cache": true,
"outputs": [],
"inputs": [
"default",
"{projectRoot}/tsconfig*.json",
"{projectRoot}/package.json",
"{workspaceRoot}/tsconfig.base.json",
"^default",
{
"dependentTasksOutputFiles": "**/*.{d.ts,d.cts,d.mts,tsbuildinfo}",
"transitive": true,
},
{
"externalDependencies": ["typescript", "@angular/compiler-cli"],
},
],
},
},
}One thing to get right: the first input must be default, not production.
production drops *.spec.ts, which would under-hash the spec sources the check
covers, so a spec-only edit could then reuse a stale cache and pass when it
should fail.
Executor options
| Option | Type | Default | Description |
| ------------- | ------- | ---------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| tsConfig | string | (required) | Path to the tsconfig to type-check. Resolved relative to the workspace root when not absolute. |
| includeDeps | boolean | false | Include out-of-project and node_modules diagnostics. The default excludes them (project-in-isolation). |
| maxWarnings | number | (unset) | Fail when the warning count exceeds this number. 0 fails on any warning. A dropped warning on an uncovered first-party file counts toward this total too. Omit to never fail on warnings alone. |
| failFast | boolean | false | Report only the first error (output brevity). Not a speed-up; all diagnostics are still gathered. |
| strict | boolean | false | Controls what a first-party file the check couldn't fully cover does to the run. Default (off): a file with an error fails the run; one with only warnings just prints a notice, though a dropped warning still counts toward maxWarnings. On: a warning-only file fails regardless of maxWarnings (a file with an error still fails). It only adds a fail path; it never turns a failure into a pass. See Partial coverage. |
Partial coverage
The check runs a project in isolation, so a few situations leave a first-party file not fully covered. It always tells you when that happens; whether the run fails depends on the case, as described below.
- A first-party file imported from source that the checked
tsconfigdoesn't declare. When your project imports an internal workspace library from source (often through a TypeScriptpathsalias), the compiler pulls that file in and checks it, but it belongs to another project, so its diagnostics are dropped from your report and you see your own errors, not the library's. An error in such a file fails the run; a warning on it just prints a notice unless you gate warnings withmaxWarningsor turn onstrict. To see the error reported directly, run that library's owntypechecktarget, or setincludeDeps: trueto fold these diagnostics back into your report. - A referenced config that declares no files: an empty config, or a
tsconfigthat only points at other projects (the check follows one level ofreferences). This fails the run rather than passing over something it never checked. Out-of-project, duplicate, and self references are skipped with a warning and don't change the result; pointtsConfigat that config directly if you need it checked. - A fatal template error (
NG3004) stops Angular's template type-checking for the whole program, which hides other files' template and NG8xxx diagnostics until you fix it. The run exits non-zero and tells you its results are incomplete. - A declared file the check can't type-check, which is advisory and does not fail the
run:
.mdxis never type-checked, and a.tsxis checked only when yourtsconfigsetscompilerOptions.jsx. The run prints a notice naming these files and stays green. SeenotTypeCheckedDeclaredFilesunder Programmatic API.
The opt-in strict option extends the first case: it makes a warning-only uncovered
file fail the run too, so any first-party file the check couldn't fully cover fails.
Output
angular-typechecker prints one format: the Angular compiler's formatDiagnostics,
a superset of tsc that renders NG codes and template code frames, written
straight to stdout. A run with a TypeScript error and an Angular template
diagnostic looks like this:
libs/ui/src/lib/greeting.component.ts:8:38 - error TS2322: Type 'number' is not assignable to type 'string'.
8 protected readonly label: string = 0;
~
libs/ui/src/lib/greeting.component.html:1:6 - error NG8002: Can't bind to 'srcc' since it isn't a known property of 'img'.
1 <img [srcc]="label" />
~~~~~~~~~~~~~~Each diagnostic is path:line:column - severity CODE: message, followed by a code
frame. The report can carry three kinds of finding, all in the same run:
- Plain TypeScript diagnostics (
TS2322and the rest of theTSxxxxset). - Angular template type-check diagnostics, from checking template expressions and
bindings against component types, such as
NG8002above. - Angular extended diagnostics: the stricter, opt-in template checks, such as
NG8101(an invalid banana-in-a-box binding) orNG8109(a signal not invoked in a template).
Three things shape the output:
- Color is auto-detected from
stdout.isTTYand stripped off-TTY (CI, pipes, agents), so captured logs stay ANSI-free. failFastcuts the reported list off at the first error. It shortens output only; every diagnostic is still gathered.- Paths are workspace-root-relative, so they line up with a standard
file:line:colproblem matcher (see Continuous integration).
Machine-readable reporters (JSON, SARIF) are a deliberate non-goal in v0.x; the human-readable format above is the only output.
Exit codes
The executor reports a pass/fail result that Nx maps to the process exit code, so a CI step or an agent can gate on pass/fail directly:
- Exit
0: no error-category diagnostics were reported, and the warning count is withinmaxWarnings. - Non-zero: at least one error-category diagnostic was reported, or the warning
count exceeded
maxWarnings. - Non-zero: the Angular compiler failed to run at all (an infrastructure error, such as a missing or unreadable tsconfig). This is logged distinctly from a type error, because a type-checker that reports success on its own crash is worse than none.
The exit code signals only pass or fail. To tell a type error, a warning-threshold failure, and an infrastructure error apart, read the output.
Continuous integration
Since the target exits non-zero on an error, a CI step that runs
nx typecheck <project> fails the job on any type or template error with no
extra scripting. Run it across only the projects a change touches with
nx affected -t typecheck.
The workspace-relative file:line:col paths also let GitHub Actions annotate each
diagnostic inline. Because the output is a tsc superset, one tsc-style problem
matcher catches both TypeScript (TSxxxx) and Angular (NGxxxx) codes. Drop this
in .github/matchers/tsc.json:
{
"problemMatcher": [
{
"owner": "angular-typechecker",
"pattern": [
{
"regexp": "^(\\S.*?):(\\d+):(\\d+)\\s+-\\s+(error|warning)\\s+((?:TS|NG)\\d+):\\s+(.*)$",
"file": 1,
"line": 2,
"column": 3,
"severity": 4,
"code": 5,
"message": 6
}
]
}
]
}Register it right before the target runs:
- run: echo "::add-matcher::.github/matchers/tsc.json"
- run: npx nx typecheck my-appProgrammatic API
Nx loads the executor and the generators by path, so most consumers never import the package directly. When you do need to run the check from code, the package exports a small barrel:
import { runTypecheck, TypecheckInfrastructureError } from 'angular-typechecker';
import type { CoreOptions, CoreResult, SkippedReference } from 'angular-typechecker';
// tsConfigPath must be absolute. runTypecheck does not resolve it for you.
const options: CoreOptions = {
tsConfigPath: '/abs/path/to/apps/my-app/tsconfig.json',
includeDeps: false, // set true to include out-of-project + node_modules diagnostics
};
try {
const result: CoreResult = await runTypecheck(options);
// result: { tsConfigPath, rootNamesCount, diagnostics: readonly ts.Diagnostic[],
// errorCount, warningCount, suppressedThirdParty, suppressedInGraphErrorCount,
// suppressedInGraphWarningCount, suppressedInGraphFiles: readonly string[],
// durationMs, templateCheckAborted?,
// skippedReferences?: readonly SkippedReference[],
// notTypeCheckedDeclaredFiles?: readonly string[],
// bundlerQueryImports?: readonly string[] }
process.exitCode = result.errorCount > 0 ? 1 : 0;
} catch (error) {
if (error instanceof TypecheckInfrastructureError) {
// the compiler crashed (code 500): an infrastructure failure, not a type error
}
throw error;
}runTypecheck hands back the raw counts and diagnostics and leaves the verdict
and rendering to you. maxWarnings, failFast, and color live in the executor,
not the barrel, and the engine internals (compiler loader, gatherer, boundary
filter, formatter) stay unexported.
How it compares
Angular builds already type-check, but they tie that check to emit. Fast dev
pipelines skip it on purpose. Per-file compilers (AnalogJS fastCompile, the
experimental Oxc compiler) and esbuild dev trade the whole-program check for
speed and tell you to run it elsewhere; in the editor, the Angular Language
Service handles the live loop. This plugin is that "elsewhere" for headless CI and
agent loops.
- Compared with
ngc --noEmit:ngcshort-circuits by phase, so a TypeScript error can mask template and NG8xxx diagnostics. angular-typechecker gathers every phase in one pass (in the spirit of@angular/build), so an ordinary TypeScript error never masks the template and NG8xxx diagnostics. - Compared with Nx's
@nx/jstypechecktarget: that runs plaintsc/tsgo, which Angular projects cannot use (Angular has no TypeScript project-references support) and which never sees template or NG8xxx diagnostics anyway.
For the background on why the whole-program type-check is the dominant, separable cost of an Angular build, see Brandon Roberts' Angular Compilation, Type-Checking, and Build Bottlenecks (2026).
Storybook
nx typecheck type-checks your Storybook stories with no extra setup. Storybook's
TypeScript files (your *.stories.ts, .storybook/main.ts, and
.storybook/preview.ts) are just more files your project's tsconfig includes, so
the tool checks them the same way it checks the rest of your project. There is no
Storybook-specific option or flag, and the plugin has no dependency on Storybook.
You get the complete Angular check on every TypeScript file your Storybook
tsconfig declares: TypeScript errors plus template and NG8xxx diagnostics, with
no emit. A passing run means all of those files type-checked cleanly. One thing
makes this work, and the configuration generator already sets it up for you.
Point the typecheck target at your project's top-level tsconfig.json, the one
with a references array, not at a specific tsconfig.app.json /
tsconfig.lib.json. The tool follows those references to find your stories, so a
single leaf config leaves the stories out. Order doesn't matter: the target reads
the references each time it runs, so adding Storybook after you wire typecheck
works on the next run, with no re-generation.
This covers both ways teams set up Storybook in an Nx workspace:
- A Storybook in each project (
nx g @nx/angular:storybook-configuration): that project's stories are checked. - One central Storybook for the whole workspace (the Nx "one Storybook for all
projects" recipe, where a single Storybook pulls in stories and components from
many projects): the files its
tsconfigdeclares are checked too, and a first-party file reached only through an import is surfaced as incomplete coverage rather than skipped. A real error in any of them fails the run.
Storybook Composition
Storybook Composition (one Storybook that embeds other, independently built
Storybooks) is a project structure, not a special tsconfig. Each composed project
and the composing host are ordinary per-project Storybooks, so you check them the
normal way: give each project its own typecheck target and run them together with
nx run-many -t typecheck or nx affected -t typecheck. To check a host and
everything it composes in one command, add dependsOn: ["^typecheck"] to the
host's typecheck target and list the composed projects in the host's
implicitDependencies.
This checks each project's own TypeScript, including the host's
.storybook/main.ts, so a real TypeScript error in its refs object is reported.
It does not verify that the composed refs URLs resolve or deploy; those are
runtime URLs, not TypeScript. Note that @storybook/angular types the refs
object loosely (as any), so a mistyped refs value is caught only if you
annotate it with your own type.
What this does and doesn't promise
It type-checks the TypeScript files your Storybook tsconfig declares. It does not
claim to cover every Storybook file, and it does not verify that Storybook builds or
runs. It is a type-check, nothing more. Setups other than the two above aren't
officially supported. When it can't fully check a first-party file it says so;
whether that fails the run depends on the case (see Partial coverage).
Things to know
.mdxdocs are never type-checked, and a.tsxstory is checked only when yourtsconfigsetscompilerOptions.jsx. When your Storybook config declares files like these that can't be checked,nx typecheckprints a notice naming them; this never changes whether the run passes. SeenotTypeCheckedDeclaredFilesunder Programmatic API.Stories that use an external
templateUrlare handled. A template error such asNG8002is reported against the right component, not dropped.Vite and Analog Storybook
?queryimports: add"types": ["vite/client"]to the tsconfig you check. That one line is the fix. Imports likeimport src from './x?raw'(and?url,?worker,?inline, and virtual modules) are Vite features TypeScript doesn't understand on its own, so the Angular compiler reports them asTS2307("cannot find module"). Adding"vite/client"declares that whole family of imports and clears the errors. In one real project it took the count from 227 such errors to zero without hiding any genuine problem: a truly missing module, or the wrong type of value, still fails. This applies whether your Storybook builds with webpack/esbuild (@storybook/angular) or Vite (@analogjs/storybook-angular). It is standard TypeScript behavior for Vite projects, not something specific to this tool.If you'd rather not depend on Vite's types, declare the imports yourself in a
.d.tsyourtsconfigincludes, one per suffix (declare module '*?raw' { const src: string; export default src; }). This only covers the suffixes you list, so"vite/client"is preferred. Either way there is one narrow blind spot: a?queryimport of a base file that doesn't exist won't be flagged, the same gap between building and type-checking that Vite itself has.The tool never hides these
TS2307errors automatically, because a missing module can be a real bug. When it sees unresolved?queryimports it lists them in a notice that points you at this same fix and goes quiet once they resolve (seebundlerQueryImportsunder Programmatic API).Point the target at your top-level
tsconfig.json, not atsconfig.app.json/tsconfig.lib.json. A leaf config leaves the stories out.A single flat
tsconfig.jsonwith noreferencesisn't an officially supported Storybook setup. Pointed at a flat config directly, the tool checks the stories that config includes; a config that declares no files fails the run instead of passing with nothing checked.The Angular CLI Storybook setup (
ng add @storybook/angular, which wires its tsconfigs throughangular.jsonwith no top-levelreferences) is not supported.Installing Storybook on Angular 22 needs a peer-dependency override.
@storybook/[email protected]still caps its Angular peer at>=18 <22(TypeScript^4.9 || ^5), so you need--legacy-peer-deps(or--force) to install it on Angular 22 / TypeScript 6; on pnpm,nx addcan hitERR_PNPM_IGNORED_BUILDS. This is a Storybook install constraint, not an angular-typechecker one; the tool applies no version gate. That Storybook version also emits 48 TypeScript 6 errors from its own bundled type declarations. Those come fromnode_modulesand never affect your result, while genuine errors in your ownmain.ts/preview.tsare still reported.
Limitations
- angular-typechecker is 0.x (pre-1.0). Breaking changes are allowed in minor releases under the project's 0.x semver policy.
- Some first-party files can't be fully covered when a project is checked in isolation; see Partial coverage for the cases and how to control them.
- Machine-readable reporters (JSON, SARIF) and a standalone CLI are non-goals in v0.x.
Contributing
Issues and pull requests are welcome on GitHub. Please open an issue to discuss a substantial change before sending a pull request.
License
MIT (c) 2026 Lars Gyrup Brink Nielsen
