Build & Toolchain
Type-checking with tsc versus transpiling with esbuild or SWC, the Go-native TypeScript 7 compiler, project references and incremental builds in a monorepo, typescript-eslint, and diagnosing slow type-checking.
18 questions
JuniorTheoryVery commonWhat does tsc do that transpilers like esbuild and SWC skip?
What does tsc do that transpilers like esbuild and SWC skip?
tsc type-checks the whole program, then emits. esbuild and SWC only strip types and emit — they skip type-checking entirely. They see one file at a time, with no cross-file view, which is why isolatedModules exists.
Common mistakes
- ✗Assuming a green
esbuildbuild means the types are valid - ✗Treating
isolatedModulesas a speed flag rather than a single-file-emit constraint - ✗Believing
tsc --noEmitin CI is redundant once a fast transpiler is in the pipeline
Follow-up questions
- →Which TypeScript constructs does
isolatedModulesforbid, and why exactly those? - →Where in a pipeline would you place
tsc --noEmitrelative to the bundleresbuild?
MiddleTheoryCommonWhat does the type-aware linter typescript-eslint catch that tsc never will?
What does the type-aware linter typescript-eslint catch that tsc never will?
tsc only checks type correctness — code can be type-safe and still awful. typescript-eslint layers policy on the same type information: floating promises, unsafe any access, naming and idiom rules. Those are choices, not type errors.
Common mistakes
- ✗Assuming a clean lint run implies the types check, or the other way round
- ✗Believing the linter is syntax-only and cannot use type information
- ✗Enabling type-aware rules without a
projectsetting, which silently disables them
Follow-up questions
- →Why do type-aware rules need a
tsconfigand cost far more time than syntactic ones? - →Which rule catches a promise whose rejection is never handled?
MiddleDesignCommonYour monorepo holds twelve packages: several libraries, two Node services and one React app. Today a single root tsconfig.json lists every source folder in one program, and the team is arguing about whether to keep that or give each package its own tsconfig.json that extends a shared base. Constraints: the libraries must ship as compiled packages with declarations; the React app needs jsx and DOM library types that the Node services must never see; CI must fail on a type error in any package; and a change to one library should not force the whole repo to be re-checked. Compare the two layouts, say which you would adopt and why, and state what belongs in the shared base versus in each package's own config.
Your monorepo holds twelve packages: several libraries, two Node services and one React app. Today a single root tsconfig.json lists every source folder in one program, and the team is arguing about whether to keep that or give each package its own tsconfig.json that extends a shared base. Constraints: the libraries must ship as compiled packages with declarations; the React app needs jsx and DOM library types that the Node services must never see; CI must fail on a type error in any package; and a change to one library should not force the whole repo to be re-checked. Compare the two layouts, say which you would adopt and why, and state what belongs in the shared base versus in each package's own config.
A shared base config each package extends, wired by project references. The base holds repo-wide policy: strict, target, module. Each package owns what differs — lib, jsx, outDir. One program cannot give Node and DOM different libs.
Common mistakes
- ✗Letting DOM types leak into Node services through one repo-wide
libsetting - ✗Duplicating
strictand target in every package instead of extending a shared base - ✗Expecting one root program to rebuild per package without a reference graph
Follow-up questions
- →What belongs in the base config, and what must stay per-package?
- →How would you stop a Node service importing a browser-only library package?
MiddleTheoryCommonWhat do project references in tsconfig buy you in a monorepo?
What do project references in tsconfig buy you in a monorepo?
They split the repo into separately-built projects with a declared dependency graph. Each project is checked once and emits .d.ts files that its dependents consume instead of source, so tsc --build rebuilds only the stale projects.
Common mistakes
- ✗Confusing
referenceswithpaths— one builds a graph, the other only aliases specifiers - ✗Forgetting a referenced project must set
composite: trueand emit declarations - ✗Running plain
tsc, which ignores the reference graph, instead oftsc --build
Follow-up questions
- →What does
composite: truerequire of a referenced project? - →How does
tsc --builddecide that a referenced project is out of date?
MiddleDebuggingOccasionalWhy is AppError not assignable to AppError after adding one dependency?
Why is AppError not assignable to AppError after adding one dependency?
Two copies of @acme/errors are on disk, so the compiler builds two unrelated AppError types. Structural matching cannot help: a private member makes the class nominal, and private members from separate declarations never match. Deduplicate it.
Common mistakes
- ✗Assuming structural typing makes two identically-shaped classes interchangeable, ignoring
private - ✗Blaming a version mismatch when both copies are in fact the same version
- ✗Silencing the error with
as anyinstead of deduplicating the dependency
Follow-up questions
- →Which command shows you that a package resolved twice in the dependency tree?
- →Why does a
privatefield make an otherwise structural class behave nominally?
MiddleTheoryOccasionalWhat does incremental cache in .tsbuildinfo, and when does that cache go stale?
What does incremental cache in .tsbuildinfo, and when does that cache go stale?
It records each input version hash plus the dependency and declaration-signature graph. Next run, tsc re-checks only changed files and their dependents. Changing options or the compiler version discards it; deleting the output leaves it stale.
Common mistakes
- ✗Thinking
.tsbuildinfocaches emitted JavaScript rather than checking state - ✗Expecting an option or compiler-version change to still be reused incrementally
- ✗Deleting the build output but keeping the cache, so
tscskips a needed emit
Follow-up questions
- →Why does
tsc --buildalso compare output timestamps against its inputs? - →Should
.tsbuildinfobe committed to version control, or ignored?
MiddlePerformanceOccasionalHow do you find which project in a monorepo's graph is slow to type-check?
How do you find which project in a monorepo's graph is slow to type-check?
Measure, do not guess. tsc --build --diagnostics reports check time and file counts per project; --extendedDiagnostics adds instantiation and memory counters. For the worst project, --generateTrace shows which types and files dominate.
Common mistakes
- ✗Guessing from source-file count instead of measuring check time per project
- ✗Assuming the largest package is automatically the slowest to check
- ✗Ignoring the instantiation counter, where a single generic can dominate the run
Follow-up questions
- →What does a huge type-instantiation count in
--extendedDiagnosticsusually indicate? - →How would you narrow a slow check down from a whole project to a single file?
MiddleTheoryOccasionalWhat does the TypeScript 7.0 compiler actually solve, and what does it not change?
What does the TypeScript 7.0 compiler actually solve, and what does it not change?
It attacks latency: cold type-check time on a large repo and editor responsiveness improve several-fold. It does not touch the type system — the same programs pass and fail, with the same diagnostics. It is a speed fix, not a semantics fix.
Common mistakes
- ✗Expecting new type-system features to arrive with the Go port
- ✗Assuming a program's errors change under the new compiler
- ✗Thinking the speedup helps emit or bundling rather than checking
Follow-up questions
- →How would you measure the cold-check improvement on your own repository?
- →If a diagnostic differs between TypeScript 6.0 and 7.0, is that a feature or a bug?
SeniorDesignOccasionalYou maintain a ten-year-old TypeScript codebase. Its root tsconfig.json still sets target: es5, module: umd, moduleResolution: node10, a baseUrl for imports, an outFile for a legacy bundle and esModuleInterop: false; a second config uses module: amd for an old plugin host. The team upgraded to TypeScript 6.0, saw nothing but deprecation warnings, and now assumes TypeScript 7.0 is a routine bump. Describe the audit you would run before that bump: what specifically will stop compiling, why the TypeScript 6.0 run gave the team no useful signal, in what order you would remove the dependencies on those options, and how you would prove the repository is ready.
You maintain a ten-year-old TypeScript codebase. Its root tsconfig.json still sets target: es5, module: umd, moduleResolution: node10, a baseUrl for imports, an outFile for a legacy bundle and esModuleInterop: false; a second config uses module: amd for an old plugin host. The team upgraded to TypeScript 6.0, saw nothing but deprecation warnings, and now assumes TypeScript 7.0 is a routine bump. Describe the audit you would run before that bump: what specifically will stop compiling, why the TypeScript 6.0 run gave the team no useful signal, in what order you would remove the dependencies on those options, and how you would prove the repository is ready.
TypeScript 6.0 deprecated them; 7.0 makes them hard errors: es5, AMD, UMD, systemjs, moduleResolution: node10/classic, baseUrl, outFile, downlevelIteration, esModuleInterop: false. A clean 6.0 build proves nothing; audit all configs.
Common mistakes
- ✗Reading a warning-free TypeScript 6.0 build as proof that the 7.0 bump will pass
- ✗Auditing only the root
tsconfigand missing the configs that extend or override it - ✗Assuming resolution options like
baseUrlsurvive because only emit options were removed
Follow-up questions
- →What replaces
baseUrlonce it becomes a hard error? - →How would you keep shipping an ES5 build once the compiler refuses to emit one?
SeniorDebuggingOccasionalThe editor takes 30s to show errors in one package. What do these numbers say?
The editor takes 30s to show errors in one package. What do these numbers say?
Parse and bind take two seconds; check takes ninety-six, and 48 million instantiations across 1842 files is wildly out of proportion. That is a type-level blow-up, not file count and not disk. Take a --generateTrace and find the generic.
Common mistakes
- ✗Blaming file count or disk when parse and bind time are already near zero
- ✗Reading the large memory figure as the cause rather than a consequence of instantiation
- ✗Guessing at the culprit generic instead of taking a trace and looking
Follow-up questions
- →What does the assignability-cache size tell you when read next to the instantiation count?
- →Which type constructs most often produce an instantiation blow-up like this one?
SeniorDesignOccasionalA twenty-package monorepo already uses project references, yet touching one line in the shared @acme/core package re-checks almost everything downstream — a nine-minute CI step for a comment change. Everything depends on core, core re-exports the types of three other packages through a barrel file, and packages emit declarations with declaration: true but set composite inconsistently. Explain what actually makes tsc --build decide a dependent is out of date, then restructure the reference graph and the packages themselves so an ordinary change inside core no longer forces the downstream closure to rebuild.
A twenty-package monorepo already uses project references, yet touching one line in the shared @acme/core package re-checks almost everything downstream — a nine-minute CI step for a comment change. Everything depends on core, core re-exports the types of three other packages through a barrel file, and packages emit declarations with declaration: true but set composite inconsistently. Explain what actually makes tsc --build decide a dependent is out of date, then restructure the reference graph and the packages themselves so an ordinary change inside core no longer forces the downstream closure to rebuild.
tsc --build re-checks a dependent only when the dependency emits a changed .d.ts signature, so keep implementation edits out of the public declarations: set composite everywhere, split core into types plus implementation, drop the barrel.
Common mistakes
- ✗Thinking a dependent is invalidated by a source edit rather than by a changed declaration signature
- ✗Leaving
compositeunset, which disables the up-to-date check that references rely on - ✗Keeping a barrel that re-exports everything, so any change ripples into the whole graph
Follow-up questions
- →Why does splitting a types-only package out of
coreshorten the rebuild chain? - →What does
composite: truechange about howtsc --builddecides staleness?
JuniorTheoryRareWhy is the Go-native TypeScript 7.0 compiler faster than the TypeScript 6.0 one?
Why is the Go-native TypeScript 7.0 compiler faster than the TypeScript 6.0 one?
TypeScript 7.0 ports the same checker to Go: it runs as native code instead of JavaScript and uses real parallelism across cores. It is a port, not a redesign — the type system, the syntax and the diagnostics are unchanged. Only latency changes.
Common mistakes
- ✗Thinking the port changes the type system or its inference rules
- ✗Expecting different diagnostics or a different set of accepted programs from TypeScript 7.0
- ✗Assuming the speed comes from cutting checks rather than from native code and parallelism
Follow-up questions
- →Which workload gains more — a cold check in CI or an incremental update in the editor?
- →Would a program that fails to compile under TypeScript 6.0 still fail under 7.0?
SeniorDesignRareYour platform team owns a codegen tool and a set of custom lint rules that both import * as ts from 'typescript' and walk the AST through the compiler API. Leadership has read the TypeScript 7.0 benchmarks and wants the whole repository moved onto it this quarter; CI today runs one tsc --build over twenty packages and takes eleven minutes. Explain what is actually possible right now given the state of TypeScript 7.0 and what is not, then design the plan you would put in front of leadership: what moves immediately, what stays on TypeScript 6.0 and why, how the two compiler versions coexist inside one repository, and what concrete signal tells you the migration can finally be completed.
Your platform team owns a codegen tool and a set of custom lint rules that both import * as ts from 'typescript' and walk the AST through the compiler API. Leadership has read the TypeScript 7.0 benchmarks and wants the whole repository moved onto it this quarter; CI today runs one tsc --build over twenty packages and takes eleven minutes. Explain what is actually possible right now given the state of TypeScript 7.0 and what is not, then design the plan you would put in front of leadership: what moves immediately, what stays on TypeScript 6.0 and why, how the two compiler versions coexist inside one repository, and what concrete signal tells you the migration can finally be completed.
You cannot migrate the API consumers: TypeScript 7.0 ships no programmatic API — it is targeted for 7.1. Keep 6.0 as the library the codegen and lint rules import, and run 7.0 alongside as a --noEmit checker in CI. Finish when 7.1 lands the API.
Common mistakes
- ✗Assuming a GA release means the compiler API moved with it
- ✗Planning a one-shot repo-wide version bump when two compilers must coexist
- ✗Throwing away type-aware tooling just to escape the API dependency
Follow-up questions
- →How do you keep two TypeScript versions installed in one repo without them colliding?
- →What would you measure to prove the second, non-blocking check is worth keeping?
SeniorTheoryRareTypeScript 7.0 is GA — why does typescript-eslint still pin TypeScript 6.0?
TypeScript 7.0 is GA — why does typescript-eslint still pin TypeScript 6.0?
Because TypeScript 7.0 shipped with no programmatic compiler API; a new one is targeted for 7.1. Anything that drives the checker in-process — typescript-eslint, Volar, Angular — has nothing to call. The CLI is usable; the library is not.
Common mistakes
- ✗Assuming the ecosystem pin is a scheduling choice rather than a missing API
- ✗Believing TypeScript 7.0's semantics differ, and that this is what forces the pin
- ✗Treating the situation as resolved simply because the compiler reached GA
Follow-up questions
- →Which other tools are blocked by exactly the same missing API?
- →What can you still adopt from TypeScript 7.0 today without any programmatic API?
SeniorDesignRareCI for your repository runs tsc --build and a type-aware typescript-eslint pass; together they take fourteen minutes, and the linter pins TypeScript 6.0 because it loads the compiler as a library. You want the speed of TypeScript 7.0 now, but you cannot drop the linter and you cannot afford a false CI failure from a compiler the ecosystem has not caught up with. Design how you would introduce TypeScript 7.0 into this pipeline: where exactly it runs, what it is allowed to block, how both compiler versions live side by side in one repository, and what evidence would let you promote it to the blocking check.
CI for your repository runs tsc --build and a type-aware typescript-eslint pass; together they take fourteen minutes, and the linter pins TypeScript 6.0 because it loads the compiler as a library. You want the speed of TypeScript 7.0 now, but you cannot drop the linter and you cannot afford a false CI failure from a compiler the ecosystem has not caught up with. Design how you would introduce TypeScript 7.0 into this pipeline: where exactly it runs, what it is allowed to block, how both compiler versions live side by side in one repository, and what evidence would let you promote it to the blocking check.
Add TypeScript 7.0 as a second, non-blocking --noEmit job beside the existing build, while the linter keeps its 6.0 dependency. Install both versions under separate aliases. Promote 7.0 to blocking once its verdicts have matched 6.0's on real PRs.
Common mistakes
- ✗Pointing the type-aware linter at TypeScript 7.0, which it cannot load as a library
- ✗Making the new checker blocking before its verdicts have been compared with the old one
- ✗Assuming two TypeScript versions cannot be installed side by side in one repository
Follow-up questions
- →How do you install two TypeScript versions side by side without them colliding?
- →What evidence would convince you to make the TypeScript 7.0 check blocking?
SeniorTheoryRareWhere can you actually adopt TypeScript 7.0 today, and where can you not?
Where can you actually adopt TypeScript 7.0 today, and where can you not?
Checking is the safe first move: tsc --noEmit in CI yields only a pass or fail verdict, so nothing downstream consumes its artifacts. Keep 6.0 for anything that loads the compiler as a library — 7.0 still exposes no programmatic API at all.
Common mistakes
- ✗Assuming GA means every consumer of the compiler can move at the same time
- ✗Trying to swap the compiler under a tool that imports it as a library
- ✗Expecting different verdicts from the new checker and holding CI back because of it
Follow-up questions
- →How would you run the new checker in CI without blocking the existing build?
- →Which of your tools import
typescriptas a library rather than shelling out totsc?
SeniorTheoryRareWhy is the TypeScript 7.0 compiler a Go port rather than a Rust rewrite?
Why is the TypeScript 7.0 compiler a Go port rather than a Rust rewrite?
Because it is a port, not a rewrite. The existing checker is written in TypeScript, and idiomatic Go mirrors that code structure for structure — a GC and cyclic mutable graphs come free, with native speed and memory-layout control on top.
Common mistakes
- ✗Retelling a Go-versus-Rust benchmark that no primary source actually describes
- ✗Calling it a rewrite when the entire point was a mechanical port of the existing checker
- ✗Assuming the host language changed the type system or the checking algorithms
Follow-up questions
- →What does 'a port, not a rewrite' buy you when both implementations must stay in lockstep?
- →Which property of Go's memory model matters for a checker built on cyclic mutable graphs?