Why Babel Strips Your Types but Won’t Catch Your Bugs
Babel strips TypeScript types fast but skips type checking. Keep tsc --noEmit as a CI gate to catch errors before they ship.
31 Aug 2026, 10:34 UTC

Your build finishes in seconds, but a type error slips into production. The culprit is usually a fast transpiler doing a job it was never designed to do. Babel’s @babel/preset-typescript strips TypeScript annotations quickly, but it performs no type analysis. The practical engineering decision is to let Babel handle transpilation and keep tsc as a separate type-checking gate.
Babel removes types; it does not verify them
@babel/preset-typescript operates at the AST level. It deletes type annotations, interfaces, and type aliases from the syntax tree, then hands the remaining JavaScript to other presets. Because it never builds a type graph, it cannot tell you that user.email does not exist on a User interface. A Babel-only build will succeed even when the types are completely wrong.
Isolated files demand isolated-module syntax
Babel transforms each file in isolation. It cannot look across module boundaries to determine whether an import is a type or a runtime value. If you write import { User } from './types' and User is only a TypeScript interface, Babel may leave the import in the output, causing a runtime crash. The TypeScript compiler flag isolatedModules enforces the syntax Babel requires: use import type and export type for anything that exists only at compile time.
A worked example: Babel for speed, tsc for safety
The following examples assume Babel 7 and a recent TypeScript version (4.x or later). A typical setup keeps the two tools in separate scripts. This babel.config.js strips types and targets older browsers:
module.exports = {
presets: [
['@babel/preset-env', { targets: '> 0.25%, not dead' }],
'@babel/preset-typescript'
]
};
Your tsconfig.json should enable isolatedModules and disable emit so the compiler only checks:
{
"compilerOptions": {
"isolatedModules": true,
"noEmit": true,
"strict": true
}
}
In package.json, separate the concerns:
{
"scripts": {
"build": "babel src --out-dir dist --extensions \".ts,.tsx\"",
"type-check": "tsc --noEmit"
}
}
Now consider a source file that imports a type:
import type { User } from './types';
export function greet(user: User): string {
return `Hello, ${user.name}`;
}
Run npm run build from your project root, replacing src and dist with your actual source and output directories. Standard user permissions are sufficient. Expected check: the output directory contains JavaScript with no type syntax and no ./types import. Risk: if you introduce a type error—say, referencing user.nmae—the build still succeeds because Babel does not analyze types.
Next, run npm run type-check from the same directory. Expected check: tsc reports the misspelled property and exits with a non-zero code. This is the gate that prevents broken types from reaching production.
The trade-off and known limitations
You gain significantly faster transpilation and compatibility with tools that expect JavaScript input. The cost is operational: if the separate type check is skipped, type errors ship silently. Local editor caching and bundler dev servers may not surface the missing check, delaying failures until CI.
Some TypeScript constructs are not fully supported by Babel. const enum members are inlined by tsc but not by Babel, which can change runtime behavior. Parameter properties in class constructors require special handling or a tsc emit. Namespaces that contain runtime code may also fail under Babel. If your codebase relies on these features, you may need to run tsc for emit in those files or refactor to standard ES modules.
Actionable next steps
- Add
"type-check": "tsc --noEmit"to yourpackage.jsonand run it in CI before merge. - Enable
isolatedModulesintsconfig.jsonand replace value imports of types withimport type. - Inspect the compiled output for a file using
import typeto confirm the type-only import is removed. - Introduce a deliberate type error locally and verify that
npm run buildsucceeds whilenpm run type-checkfails.
Keeping Babel and tsc in separate lanes gives you fast feedback during development and a hard safety net before deployment. The moment you drop the second step, you are shipping unchecked types.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.