48.1 Running tsc --noEmit in CI for Type Checking
Right, so you’ve got your TypeScript compiling. The code runs. You’re feeling pretty good. But let me ask you a question you didn’t want to hear: are you sure you don’t have any type errors? I mean, really sure? If you’re just running tsc to emit JavaScript, you’re living on a prayer. That command will happily spit out .js files even if your types are a complete dumpster fire, as long as the syntax is vaguely correct. It’s the equivalent of saying “Well, the engine fell out, but look, the radio still works!”
This is why tsc --noEmit is your new best friend in CI. It’s the pedantic, hyper-vigilant security guard for your codebase. It does the type checking without bothering with the whole “generating output” thing. Its only job is to find problems and yell about them. And in a Continuous Integration pipeline, that’s exactly the kind of unyielding scrutiny you need.
Why --noEmit is a Non-Negotiable CI Step
Think of your CI pipeline as a series of gates. A commit doesn’t get to merge until it passes through all of them. tsc --noEmit is your first, most important gate. It answers one question: “Is this code even semantically valid TypeScript?” If it’s not, why on earth would you waste CPU cycles running linters, tests, or a deployment? You’re just going to fail later, but with more steps and more wasted time. Failing fast on type errors is the professional move. It saves resources and gives the developer immediate, unambiguous feedback: “Fix your types. Then we’ll talk.”
The Basic Command and Its Nuances
The base command is simple, but like a talented diva, it has some demands.
# The classic. It uses your tsconfig.json by default.
npx tsc --noEmit
# Be explicit. It's good practice, especially in CI where paths can be weird.
npx tsc --project ./tsconfig.json --noEmit
But here’s where people get tripped up: scope. By default, tsc will use your tsconfig.json, which might not include all the files you think it does. If you’ve used "exclude" or a sparse "include", you might be checking a happy little island of perfection while the mainland is burning down. In CI, you almost always want to check the entire project. A good practice is to have a dedicated tsconfig.ci.json that extends your base config but removes any excludes and ensures everything is included.
// tsconfig.ci.json
{
"extends": "./tsconfig.json",
"exclude": [] // Override and exclude nothing. Check it all.
}
Then, run it with:
npx tsc --project ./tsconfig.ci.json --noEmit
Integrating with Your CI Platform
The integration is straightforward because the command is your oracle; it exits with code 0 on success and >0 on failure. Every CI system on earth understands this.
Here’s a realistic example for a GitHub Actions workflow:
# .github/workflows/typecheck.yml
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: '20'
cache: 'npm'
- run: npm ci # Use 'ci' for a deterministic install in CI
- run: npx tsc --project ./tsconfig.ci.json --noEmit
See? No fancy actions needed. It’s just npx tsc. If there’s a type error, the step fails, the build is marked as failed, and everyone knows about it. Beautiful.
Performance: Because You’re Not Made of Time
On a large project, the type checker can start to feel sluggish. The TypeScript team knows this, and they’ve given us a glorious tool: --incremental. This flag (often already enabled in your tsconfig via "incremental": true) tells the compiler to save information about the state of the program in a .tsbuildinfo file. On subsequent runs, it can skip checking files that haven’t changed, which is a massive speed boost.
In CI, however, you’re usually building on a fresh runner every time. There’s no .tsbuildinfo file from a previous run! So you might think --incremental is useless. Ah, but you can cache it.
# In your GitHub Actions step, add a cache step:
- name: Cache TypeScript incremental data
uses: actions/cache@v4
with:
path: .tsbuildinfo # Or wherever your tsconfig outputs it
key: ${{ runner.os }}-ts-incremental-${{ hashFiles('**/tsconfig.ci.json') }}
restore-keys: |
${{ runner.os }}-ts-incremental-
This will restore the .tsbuildinfo file from a previous run, allowing the compiler to truly work incrementally and shave precious seconds off your CI time. It’s a small configuration win that feels like a major victory.
The Pitfalls and The “Wait, What?” Moments
skipLibCheck: A Deal with the Devil. You might have"skipLibCheck": truein yourtsconfigto speed things up. This is usually fine… until it’s catastrophically not. It skips type checking of yournode_modulesdependencies. If one of those libs has a bad type definition,tsc --noEmitwill miss it. For CI, consider setting"skipLibCheck": falseto be absolutely thorough. It’s slower, but safe.The Phantom
d.tsFiles. Remember,tscfollows yourtsconfig. If you’re generating declaration files (.d.ts) with"declaration": true,--noEmitwill surprisingly still emit those declaration files! It’s the one exception to the “no emit” rule. This is, frankly, a bit absurd. If you absolutely want zero file system changes, you need to also add--emitDeclarationOnly false.
So, the full, pedantic, “I-want-zero-surprises” command is:
npx tsc --project ./tsconfig.ci.json --noEmit --emitDeclarationOnly false
It’s verbose. It’s overkill for most. But it’s precise. And in the world of CI, precision is what keeps the ship from sinking. Now go configure it. Your brilliant-but-sloppy future self will thank your diligent current self when the build fails before it gets to production.