
When TypeScript Type Errors Aren't Caught During Build
Solving issues where TypeScript type errors show during development but are ignored during build.

Solving issues where TypeScript type errors show during development but are ignored during build.
Anatomy of the GCC pipeline. Preprocessor, Compiler, Assembler, and Linker. What happens when you type `gcc main.c`.

Stop using 'any'. How to build reusable, type-safe components using Generics. Advanced patterns with extends, keyof, and infer.

Fixing the crash caused by props coming through as undefined from the parent component.

Do you know what the Circle (○) and Lambda (λ) symbols mean in Next.js build logs? Ensure you aren't accidentally making every page dynamic.

While developing with TypeScript, VSCode showed type errors with red underlines. Thinking "I'll fix it later," I ran the build, and the build succeeded.
// VSCode shows error
const user: User = { name: 'John' }; // ❌ Property 'age' is missing
// But build succeeds
npm run build
✓ Build successful!
Worse, runtime errors occurred after deploying to production. The whole point of TypeScript is type safety, but if type checking isn't done during build, it's meaningless.
At first I thought "doesn't TypeScript automatically type-check?" But turns out build tools can skip type checking depending on configuration.
I assumed using TypeScript meant automatic type checking. But confusing parts:
tsc and build tools?
Especially "Next.js claims TypeScript support, so why doesn't it type-check?"
The turning point was accepting "build tools skip type checking for speed."
I understood this through the "translation vs proofreading" analogy:
tsc): Translation + proofreading. Finds grammar errors (type errors) and reports them. Slow.graph LR
A[TypeScript Code] --> B{Build Tool}
B --> C[tsc]
B --> D[Vite/esbuild]
C --> E[Type Check + Transform]
D --> F[Transform Only, No Type Check]
E --> G[Slow but Safe]
F --> H[Fast but Risky]
style E fill:#9f9,stroke:#333
style F fill:#f99,stroke:#333
Most modern build tools (Vite, esbuild, SWC) don't type-check, they only transform TypeScript to JavaScript. Reason: speed.
tsc: Type check + transform → Slow (tens of seconds)esbuild: Transform only → Fast (seconds)Run type check separately in package.json:
{
"scripts": {
"dev": "vite",
"build": "tsc --noEmit && vite build",
"type-check": "tsc --noEmit"
}
}
Explanation:
tsc --noEmit: Type check only, don't generate files&&: Build proceeds only if type check succeedsNow build fails if type errors exist:
npm run build
# On type error
error TS2741: Property 'age' is missing in type '{ name: string; }'
# Build stops!
tsconfig.json Strict ModeMake type checking stricter:
{
"compilerOptions": {
"strict": true,
"noImplicitAny": true,
"strictNullChecks": true,
"strictFunctionTypes": true,
"strictPropertyInitialization": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noImplicitReturns": true,
"noFallthroughCasesInSwitch": true
}
}
Next.js doesn't type-check during build by default. To enable:
// next.config.js
module.exports = {
typescript: {
// ⚠️ Dangerous: ignore type errors and build
ignoreBuildErrors: false, // default is false but set explicitly
},
};
Or add to build script:
{
"scripts": {
"build": "tsc --noEmit && next build"
}
}
Vite doesn't type-check by default. Use plugin:
npm install -D vite-plugin-checker
// vite.config.ts
import { defineConfig } from 'vite';
import checker from 'vite-plugin-checker';
export default defineConfig({
plugins: [
checker({
typescript: true, // Enable type checking
}),
],
});
Now type errors show as overlay during development.
GitHub Actions example:
# .github/workflows/ci.yml
name: CI
on: [push, pull_request]
jobs:
type-check:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
with:
node-version: '18'
- run: npm install
- run: npm run type-check # Run type check
- run: npm run build
This catches type errors before merging PRs.
Type check before commit:
npm install -D husky lint-staged
npx husky install
npx husky add .husky/pre-commit "npx lint-staged"
// package.json
{
"lint-staged": {
"*.{ts,tsx}": [
"tsc --noEmit",
"eslint --fix"
]
}
}
Now commits are blocked if type errors exist.
When adding type checking to existing projects, do it gradually:
// tsconfig.json
{
"compilerOptions": {
"strict": false, // Start with false
"noImplicitAny": true, // Enable one by one
"strictNullChecks": false
}
}
Fix errors while increasing options.
tsc is slow, so use caching:
{
"compilerOptions": {
"incremental": true, // Incremental compilation
"tsBuildInfoFile": ".tsbuildinfo"
}
}
This only type-checks changed files.
Run build and type check in parallel:
npm install -D concurrently
{
"compilerOptions": {
"strict": true
},
"scripts": {
"build": "concurrently \"tsc --noEmit\" \"vite build\""
}
}
But beware: build proceeds even with type errors.
Make type errors clearer in VSCode:
// .vscode/settings.json
{
"typescript.tsdk": "node_modules/typescript/lib",
"typescript.enablePromptUseWorkspaceTsdk": true,
"editor.codeActionsOnSave": {
"source.fixAll.eslint": true
}
}
ignoreBuildErrors: trueSet Next.js to ignore type errors, then got production errors.
// ❌ Never do this!
module.exports = {
typescript: {
ignoreBuildErrors: true, // Dangerous!
},
};
Lesson: Always fix type errors. Don't ignore them.
@ts-ignoreUsed @ts-ignore to temporarily ignore type errors, which later became real bugs.
// ❌ Bad habit
// @ts-ignore
const user: User = { name: 'John' };
Lesson: @ts-ignore is a last resort. Define types properly.
any TypeUsed any to avoid type errors.
// ❌ Giving up type safety
const data: any = fetchData();
Lesson: Use unknown instead of any, and use type guards.
// ✅ Correct way
const data: unknown = fetchData();
if (typeof data === 'object' && data !== null && 'name' in data) {
console.log(data.name);
}
skipLibCheck: trueSkipped library type checking and missed library version conflicts.
// ⚠️ Use carefully
{
"compilerOptions": {
"skipLibCheck": true // Skip node_modules type check
}
}
Lesson: Use skipLibCheck for build speed, but manage library versions well.
# Output all type errors
tsc --noEmit
# Check specific file only
tsc --noEmit src/components/Button.tsx
Hover over variables in VSCode to see inferred types.
const user = { name: 'John', age: 30 };
// Hover shows: const user: { name: string; age: number; }
// Cmd/Ctrl + click to go to type definition
import { User } from './types';
TypeScript type errors aren't caught during build because build tools skip type checking. Add tsc --noEmit to your build script.