Build & Emit
Checking and emitting are different tools’ jobs
Section titled “Checking and emitting are different tools’ jobs”The module opened with the idea that tsc does two jobs — check and emit. In a modern setup those jobs are usually done by different tools, and understanding why makes the config obvious.
flowchart LR src["source .ts"] --> bundler["bundler (esbuild/swc/vite) strips types, emits JS fast"] src --> tsc["tsc --noEmit checks types"] bundler --> out["shipped JavaScript"] tsc --> ci["pass/fail in CI"]
- The bundler (esbuild, swc, Vite) transpiles each file, stripping types. It is extremely fast because it never checks anything — it looks at one file at a time and deletes the types.
tsc --noEmitdoes the whole-program type check and produces no output. This is what actually catches your bugs, and you run it in CI (and your editor runs it continuously).
Ship what the bundler emits; trust what tsc reports. noEmit tells tsc to be a pure checker.
isolatedModules: making single-file transpilation safe
Section titled “isolatedModules: making single-file transpilation safe”Because bundlers transpile one file at a time, they cannot see types from other files. A few TypeScript constructs are ambiguous under single-file transpilation — most importantly, re-exporting a type looks identical to re-exporting a value. isolatedModules: true makes tsc flag anything a single-file transpiler could get wrong, so your code stays compatible with esbuild and swc.
// with isolatedModules, tsc forces you to be explicit here:export type { User } from "./user"; // ✅ clearly a type re-exportexport { User } from "./user"; // ❌ ambiguous: is User a type or a value?Type-only imports
Section titled “Type-only imports”The fix above is part of a broader feature: type-only imports and exports. Marking an import as type-only tells the compiler it exists purely for type information and must be erased — never emitted as a runtime import.
import type { User } from "./user"; // erased entirely at emitimport { type User, save } from "./db"; // User erased, save keptverbatimModuleSyntax: true enforces this discipline: any import that is not marked type is emitted verbatim as a runtime import. It removes a whole category of surprises where the compiler silently dropped or kept an import, and it is the recommended setting for new projects.
Project references for monorepos
Section titled “Project references for monorepos”In a large repo with several packages, type-checking everything from scratch on every change is slow. Project references let you split the codebase into independently buildable projects with declared dependencies:
{ "references": [{ "path": "../core" }], "compilerOptions": { "composite": true }}With composite: true, tsc --build checks each referenced project once, caches the result, and only rechecks what changed. It turns an O(whole-repo) check into an incremental one — the standard way to keep type-checking fast as a monorepo grows.