TypeScript Tooling and Project Workflow
TypeScript usually runs inside a framework or bundler, which is convenient right up to the moment something behaves strangely. Knowing which tool actually checks your types, which one merely removes them, and where each reads its settings turns most of those moments into a two-minute answer.
This guide covers the compiler commands, the split between transpiling and checking, the editor's role, wiring a real check into CI, and migrating an existing JavaScript codebase without stalling.
The Commands Worth Knowing
Four invocations cover nearly everything a project needs:
# type check the project, write nothing
tsc --noEmit
# re-check on save while you work
tsc --noEmit --watch
# build a workspace of referenced projects in dependency order
tsc --build
# show the resolved config, with extends and computed options applied
tsc --showConfig The first is the one to memorise: it reads tsconfig.json, checks every file in the program, and reports errors without touching your output directory. The --noEmit flag never writes files; drop it in a package that tsc itself compiles and you get output too, unless noEmit is set in the config.
tsc --showConfig earns its place during an argument. It prints the configuration the compiler actually resolved, including inherited values and computed options, though it does not enumerate every implicit default, and it settles most questions about why a file is or is not being checked faster than reading a chain of extends by hand.
Transpiling Is Not Checking
Fast build tools are fast partly because they skip the expensive half. A transpiler processes one file at a time, deletes the type annotations, and emits JavaScript. Plenty of type errors are local to a single file, yet a transpiler reports none of them, because it skips semantic checking altogether and complete checking also needs the other files:
// src/total.ts
export function total(prices: number[]): number {
return prices.reduce((sum, price) => sum + price, 0);
}
// src/checkout.ts
import { total } from "./total";
console.log(total("19.99")); # esbuild: emits JavaScript, reports nothing
esbuild src/checkout.ts --bundle --outfile=dist/checkout.js
# tsc: refuses
tsc --noEmit
# error TS2345: Argument of type 'string' is not assignable to parameter of type 'number[]'. Neither tool is wrong; they are answering different questions. The problem is assuming the fast one answered both, which is how a project runs for months believing it is type checked.
Frameworks differ on this. Some run a checker alongside the dev server or during the production build, some deliberately leave checking to the editor and CI for speed, and some make it a configuration choice. Check your own setup by introducing a deliberate type error and seeing whether the build fails.
Node's own handling follows the same rule. Since 23.6, backported to 22.18 and stable since 24.12, Node runs TypeScript files directly by stripping types with no flag, and tools such as tsx and Deno do the equivalent.
Node strips erasable syntax only, so enums, namespaces, and parameter properties are refused, it ignores tsconfig.json, and it will not strip TypeScript under node_modules. Stripping is not checking either, so those runtimes execute code the compiler would reject.
The Editor and the Language Service
The red squiggles in your editor come from the TypeScript language service, the same type checker exposed as a long-running server that answers questions about the file you are looking at. That is why autocomplete, rename, go-to-definition, and diagnostics all improve together when your types get more precise.
Two details explain most editor confusion. VS Code and some IDEs ship a bundled TypeScript version and use it unless configured to prefer the workspace copy, while other editors reach a workspace or global compiler through a language server, so an editor can be a major version ahead of or behind your build. And the service resolves the nearest tsconfig.json to the open file, which in a monorepo is often not the config CI runs.
When the editor and CI disagree, compare those two things first: the compiler version and the resolved config. A genuine compiler bug is much rarer than either.
Type Checking in CI
One script and one job are enough to make type errors a build failure rather than a suggestion:
{
"scripts": {
"typecheck": "tsc --noEmit",
"lint": "eslint .",
"build": "vite build"
}
} Run typecheck as its own CI step rather than folding it into the build, so a failure names the actual problem. Keep it separate from linting too: the two report different classes of mistake and it is useful to see which one failed at a glance.
If your package does compile with tsc, add noEmitOnError to the config. By default the compiler writes JavaScript even when it reported errors, which means a broken build can produce a shippable-looking output directory.
Linting Next to the Compiler
A linter is not a redundant second checker. The compiler answers whether types line up; a linter answers whether the code follows patterns your team wants, and some of the most valuable rules need type information to work:
- Unhandled promises, which type-check fine and fail silently at runtime.
- Unused variables and imports, which the compiler reports only under specific flags.
- Unnecessary conditions on values the types say are never nullish.
- Consistent import syntax, which matters when a transpiler processes files in isolation.
Type-aware rules require the linter to load your tsconfig.json, which makes lint runs slower than syntax-only linting. That cost is usually worth paying in CI even when local runs use a lighter configuration.
Migrating a JavaScript Codebase
A migration that stalls usually started in the wrong place. This order keeps each step small enough to finish:
- Enable
allowJsso.jsand.tsfiles coexist in one program. - Convert utilities with few dependencies of their own first, since their types then flow outward to the callers you convert later.
- Name the shared data models and the API client next, because those types unlock everything downstream.
- Move components once their props are clear, not before.
- Raise strictness after the noisy edges are typed, one flag at a time.
Two options help along the way. checkJs, or a // @ts-check comment at the top of a single file, turns on checking for JavaScript using JSDoc annotations, which lets a file gain types before it gains a new extension. And packages without bundled types often have community definitions under the DefinitelyTyped scope, installable as a @types dependency.
Resist converting the strangest file in the repo first. It is the least understood and the most likely to need real refactoring, which turns a migration into a rewrite nobody sanctioned.
Pitfalls and Debugging
A passing pipeline that never checked anything. Test it directly: add a deliberate type error, push, and see whether CI fails. If it passes, the checking step does not exist yet.
Running tsc with filenames. Since TypeScript 6, tsc src/app.ts is an error while a tsconfig.json is present, and tsc --ignoreConfig src/app.ts falls back to compiler defaults, so it can report a completely different set of errors from the project build. Run tsc --noEmit with no file arguments to reproduce what CI does.
Slow checks blamed on the wrong thing. Large unions, deeply nested conditional types, and checking inside declaration files all cost time. skipLibCheck, project references, and simplifying the worst type in the codebase usually help more than upgrading the machine.
An @types package fighting a bundled one. When a library ships its own declarations, an extra @types entry for it can conflict or go stale. Check whether the package includes types before adding one.
Frequently Asked Questions
Why did the build pass when there are type errors?
Most likely nothing in the pipeline checked types. Transpilers such as esbuild, SWC, and Babel remove annotations file by file without checking them, and tsc, when it is emitting at all, still writes output on errors unless noEmitOnError is set. A separate tsc --noEmit step is what turns a type error into a failed build.
Why does my editor show an error the build does not?
Usually a version or config mismatch. VS Code and some IDEs ship their own TypeScript rather than your project's, other editors use a workspace or global compiler, and each resolves the nearest tsconfig to the open file, which is not always the config CI runs. Point the editor at the workspace version and compare configs before assuming a compiler bug.
Do you still need ESLint with TypeScript?
They cover different ground. The compiler checks that types line up; a linter catches patterns that type-check but are still mistakes, such as an unhandled promise or an unused variable. Type-aware lint rules need the linter pointed at your tsconfig, which costs some run time and unlocks the more useful rules.
What's the fastest first step in a migration?
Turn on allowJs so both file types coexist, then convert utilities with few dependencies of their own and clear consumers. Their types flow outward to those callers as they are converted later. Starting with the most tangled file in the repo is the common way a migration stalls before anyone sees a benefit.
Related
- TypeScript Tooling for the full topic overview
- tsconfig.json for the settings every command here reads
- API Contracts and Shared Models for keeping generated types in a build pipeline
- package.json, npm, and Scripts for the package manifest, dependencies, and script running this page builds on
- TypeScript for the language guide and the full learning path
Sources
-
[1]
Compiler Options(typescriptlang.org)
-
[2]
Type Checking JavaScript Files(typescriptlang.org)
-
[3]
Node.js Modules: TypeScript(nodejs.org)
Read Next
What tsconfig.json controls, which files actually get checked, what strict mode turns on, the flags that stay off until you name them, and how to share a config across a workspace.
Modelling a request and response once, why a shared type is not validation, generating types from an OpenAPI or GraphQL schema, and changing a contract without breaking callers.
The half of TypeScript that is not types: tsconfig.json, the split between transpiling and type checking, React components, and API contracts between a frontend and its server.