The Real Role of tsc in Your Pipeline
When you're shipping TypeScript across multiple platforms—web, mobile, backend—every second counts. Your bundler strips types in milliseconds. Your runtime does the same. But verification? That's a different beast. This is where tsc as a type checker becomes your safety net. Instead of letting type errors slip into production, you run tsc --noEmit in CI to catch them early, fail the pipeline deliberately, and keep your codebase honest.
I've watched teams spend weeks debugging runtime errors that a stricter type-checking phase would have caught in CI. The payoff is immediate: fewer production incidents, faster onboarding for new developers, and confidence that your types actually mean something.
Bundlers and Runtimes Strip Types Fast
Let's be clear: bundlers like esbuild, swc, and Webpack don't perform deep type checking. They transpile TypeScript to JavaScript by stripping annotations and comments. The same goes for Node.js and browser runtimes—they execute JavaScript, not TypeScript. This is by design. These tools are optimized for speed, not verification.
esbuild, for instance, can transpile a 10,000-line TypeScript codebase in under 100ms because it's not walking your type graph or validating generic constraints. It just removes the type syntax and ships it.
// Input: TypeScript with types
interface User {
id: number;
name: string;
email?: string;
}
function createUser(user: User): User {
return user;
}
// After transpilation: pure JavaScript
function createUser(user) {
return user;
}
Your code runs perfectly in development. Your app feels snappy in build pipelines. But a subtle type error—passing a string where a number is expected, forgetting a required field, using a union type incorrectly—won't stop the build. The type annotations disappear, and JavaScript is forgiving. That's the gap we're filling with tsc --noEmit.
tsc --noEmit Checks Types in CI
The --noEmit flag tells TypeScript to skip code generation and only perform type checking. It reads your source files, builds the type graph, and reports any violations. The output? No .js files, no transpiled artifacts—just a pass or fail on types.
This is the workhorse of type safety in CI/CD pipelines. When you wire it into your pipeline, every pull request is validated against your strictest type rules before it merges.
// tsconfig.json
{
"compilerOptions": {
"strict": true,
"noEmit": true,
"skipLibCheck": false,
"resolveJsonModule": true,
"declaration": false,
"module": "esnext",
"target": "es2020"
},
"include": ["src/**/*.ts", "src/**/*.tsx"]
}
From the command line:
$ tsc --noEmit
src/services/authService.ts(42,15): error TS2345: Argument of type 'string | undefined' is not assignable to parameter of type 'string'.
src/components/UserForm.tsx(18,5): error TS2322: Type 'null' is not assignable to type 'React.ReactNode'.
Found 2 errors.
Your build fails. The developer fixes it. No guessing, no surprises at runtime. On a recent project handling ride-hailing integrations, catching type mismatches in the dispatch logic at CI time saved us from a critical payment bug in production.
Incremental Builds Speed It Up
Running full type checking on large codebases can slow down your CI. That's where incremental compilation enters. TypeScript caches type information from previous runs, so the next invocation only re-checks changed files and their dependents.
Enable it with incremental: true in your tsconfig.json:
// tsconfig.json
{
"compilerOptions": {
"incremental": true,
"tsBuildInfoFile": ".tsbuildinfo",
"strict": true,
"noEmit": true
}
}
The first run takes 15 seconds. Subsequent runs on the same files? Under a second. This is huge when your CI runs on every commit.
In practice, I've reduced type-checking time from 40 seconds to 3 seconds by enabling incremental builds and storing the .tsbuildinfo cache between CI runs. Most CI platforms (GitHub Actions, GitLab CI, CircleCI) support artifact caching, so you can persist the build info:
// GitHub Actions example
- name: Cache TypeScript build info
uses: actions/cache@v3
with:
path: |
.tsbuildinfo
node_modules/.tsbuildinfo
key: ${{ runner.os }}-tsc-${{ github.sha }}
restore-keys: |
${{ runner.os }}-tsc-
- name: Type check
run: tsc --noEmit
On a SaaS platform I maintain with 50+ services, this cut CI feedback time significantly. Developers get green checkmarks faster, and the team ships more confidently.
Fail the Pipeline on Type Errors
Here's the non-negotiable part: type errors must block merges. If your CI passes despite type failures, your type system is theater. People ignore it, complexity creeps in, and you're back to debugging runtime errors in production.
Set up your CI to fail on any tsc exit code other than 0:
#!/bin/bash
# scripts/typecheck.sh
set -e # Exit on first non-zero code
tsc --noEmit
if [ $? -ne 0 ]; then
echo "Type checking failed. Fix types before merging."
exit 1
fi
echo "Type check passed."
Or more directly in your CI config:
// .github/workflows/ci.yml example
name: CI
on: [push, pull_request]
jobs:
typecheck:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
with:
node-version: '18'
- run: npm ci
- run: tsc --noEmit
# If tsc exits with non-zero, the workflow stops here
The key is consistency. Every merge request must pass type checking. No exceptions, no "we'll fix it later." I've seen teams soften this rule and watch type safety erode in weeks. Enforce it once, maintain it forever.
Common Mistakes
Skipping skipLibCheck or setting it incorrectly: If your dependencies have type errors, `tsc` will report them by default. Set `skipLibCheck: true` to skip checking third-party type definitions unless you're auditing dependencies specifically.
Not caching incremental builds in CI: Running full type checks on every commit wastes time. Use CI caching for .tsbuildinfo to get the speed benefits of incremental builds.
Using tsc for both compilation and type checking: Don't. Use your bundler (esbuild, Webpack, Vite) for production builds. Use tsc --noEmit only for CI validation. Mixing them causes confusion about which tool is responsible for what.
Relying on IDE type checking alone: Your IDE is helpful, but it's not the source of truth. Some developers disable strict mode in their local settings while committing code that fails in CI. Make the CI rule non-negotiable.
Not configuring strict: true: Loose mode (default) allows implicit `any` and other lenient behaviors. If you're using TypeScript, commit to strict: true from the start or enable it early. Retrofitting is painful but worth it.
Key Takeaways
- Bundlers and runtimes transpile fast by ignoring types. Type verification is a separate step.
- Run
tsc --noEmitin CI as your type safety gate. It catches errors bundlers miss. - Enable incremental compilation and cache
.tsbuildinfoin CI to keep type checking fast. - Fail the pipeline on type errors unconditionally. Type safety without enforcement is just code comments.
tscis a type checker first, a compiler second. Use the right tool for each job: bundlers for building, tsc for verifying.






Comments
Loading comments…