TypeScript config in a Vite + React project
Table of Contents
- Why there are two tsconfig files
- tsconfig.json — the root coordinator
- tsconfig.app.json — browser source code
- tsconfig.node.json — Vite config (Node.js)
- Running the type checker
- Why npx tsc shows no errors
- noEmit — why TypeScript doesn't output files
- VS Code checks types automatically
- tsc: command not found
- Watch mode and the typecheck script
- The implicit any gotcha
Why there are two tsconfig files
When you scaffold a Vite + React + TypeScript project, you get three TypeScript config files:
tsconfig.json
tsconfig.app.json
tsconfig.node.json
This split exists because your project runs code in two different environments:
| File | What it checks | Environment |
|---|---|---|
tsconfig.app.json | src/ — your React components | Browser (DOM) |
tsconfig.node.json | vite.config.ts | Node.js |
These two environments need different settings. For example:
- Browser code needs
"lib": ["DOM"]so TypeScript knows aboutdocument,window, etc. - Node.js code needs Node types and has no
DOMglobals.
If you used a single config for everything, you'd either pollute your browser code with Node.js types, or your Vite config wouldn't understand Node.js APIs.
tsconfig.json — the root coordinator
{
"files": [],
"references": [
{ "path": "./tsconfig.app.json" },
{ "path": "./tsconfig.node.json" }
]
}
This file does not compile any files itself — notice "files": [].
Its only job is to declare a TypeScript Project Reference, which tells:
- VS Code: "this workspace has two sub-projects, here are their configs"
tsc --build: "build all referenced projects in the right order"
Think of it as a table of contents for your TypeScript setup.
tsconfig.app.json — browser source code
{
"compilerOptions": {
"target": "ES2022",
"lib": ["ES2022", "DOM", "DOM.Iterable"],
"module": "ESNext",
"jsx": "react-jsx",
"strict": true,
"noEmit": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
...
},
"include": ["src"]
}
Key settings:
"include": ["src"]— only check files inside thesrc/folder"lib": ["DOM"]— gives TypeScript knowledge of browser APIs (document,fetch, etc.)"jsx": "react-jsx"— enables JSX/TSX compilation for React"strict": true— enables all strict type checks"noEmit": true— do NOT output compiled.jsfiles (Vite handles that itself)"noUnusedLocals"/"noUnusedParameters"— errors on declared-but-unused variables and function parameters
This is the config that matters for your day-to-day TypeScript work in React components.
tsconfig.node.json — Vite config (Node.js)
{
"compilerOptions": {
"target": "ES2022",
"module": "ESNext",
"moduleResolution": "bundler",
"noEmit": true,
...
},
"include": ["vite.config.ts"]
}
This only checks vite.config.ts (and any helper files imported by it).
It does not include "DOM" in lib, so browser globals aren't available here — which is correct, since vite.config.ts runs in Node.js, not the browser.
Running the type checker
The -p flag
-p stands for project. It tells tsc which config file to use instead of searching for tsconfig.json in the current directory.
npx tsc -p tsconfig.app.json --noEmit
# ^^^^^^^^^^^^^^^^^^^
# use this specific config
Without -p, tsc loads the root tsconfig.json — which in a Vite project has "files": []
and checks nothing (explained in the next section). So -p is essential here.
You can point -p at any config file, or a directory containing a tsconfig.json:
npx tsc -p tsconfig.app.json # explicit file
npx tsc -p ./src # finds tsconfig.json inside ./src
Check browser source code (src/)
npx tsc -p tsconfig.app.json --noEmit
-p tsconfig.app.json— use the app config explicitly--noEmit— check types only, don't write any output files
This is the command you want when looking for TypeScript errors in your React components.
Check the Vite config
npx tsc -p tsconfig.node.json --noEmit
npx tsc --build
--build (or -b) is a different mode entirely. Instead of compiling files directly,
it reads the "references" array in tsconfig.json and builds each referenced project
in the correct order.
npx tsc --build
# reads tsconfig.json → references: [tsconfig.app.json, tsconfig.node.json]
# checks both in order
It also uses incremental compilation — it caches results in .tsbuildinfo files
and only re-checks files that changed since the last build, making it faster on repeat runs.
This is what npm run build uses internally (tsc -b && vite build).
| Command | Mode | What it does |
|---|---|---|
npx tsc -p tsconfig.app.json --noEmit | Direct | Checks one config, no output |
npx tsc --build | Project references | Checks all referenced configs, uses cache |
npx tsc | Direct (root) | Checks root config — does nothing in Vite projects |
Why npx tsc shows no errors
Running npx tsc with no flags reads the root tsconfig.json, which has:
{
"files": []
}
"files": [] means TypeScript is told to compile zero files. There's nothing to
check, so there are no errors — not because your code is correct,
but because TypeScript isn't looking at anything.
This is a common source of confusion. The root config exists only as a coordinator for project references, not as a real type-checking config.
Always run type checks against the specific config:
# ✅ Actually checks your code
npx tsc -p tsconfig.app.json --noEmit
# ❌ Checks nothing (root config has "files": [])
npx tsc
noEmit — why TypeScript doesn't output files
In a Vite project, TypeScript is used only for type checking — Vite uses esbuild
to actually compile and bundle your code, which is much faster.
esbuild strips TypeScript types without doing any type checking. That's why
you need tsc separately for type safety, but tsc doesn't need to produce output files.
This gives you the best of both worlds:
- Fast builds — esbuild compiles in milliseconds
- Type safety — TypeScript catches errors at dev time and in CI
your .tsx files
│
├──▶ tsc (type check only, noEmit: true) ──▶ errors / ✅
│
└──▶ esbuild/Vite (compile + bundle) ──▶ dist/
In your CI pipeline, you'd typically run:
npx tsc -p tsconfig.app.json --noEmit && vite build
This ensures the build only succeeds if the code is also type-safe.
VS Code checks types automatically
You don't need to run tsc to see errors. VS Code starts the TypeScript Language Server
automatically when you open a .ts or .tsx file. It reads your tsconfig.json,
analyzes your code continuously, and shows errors inline as you type.
You type code
↓
TypeScript Language Server (background)
↓
Red underline + Problems panel
Open the Problems panel with Cmd+Shift+M (Mac) / Ctrl+Shift+M (Windows).
To confirm VS Code is using your project's TypeScript version (not a built-in one):
- Press
Cmd+Shift+P - Type TypeScript: Select TypeScript Version
- Choose Use Workspace Version
This matters when your project uses a newer TypeScript than VS Code's bundled version.
tsc: command not found
If you run tsc directly in the terminal and get:
zsh: command not found: tsc
It's because TypeScript is installed locally in your project (node_modules/.bin/tsc),
not globally on your system. Always use npx to run it:
npx tsc -p tsconfig.app.json --noEmit
npx automatically finds binaries in node_modules/.bin.
If you want tsc available as a global command:
npm install -g typescript
But most teams avoid global installs to prevent version conflicts between projects.
Watch mode and the typecheck script
For continuous type checking in the terminal (re-checks on every file save):
npx tsc -p tsconfig.app.json --noEmit --watch
For everyday convenience, add a typecheck script to package.json:
"scripts": {
"dev": "vite",
"typecheck": "tsc -p tsconfig.app.json --noEmit",
"build": "tsc -b && vite build"
}
Then just run:
npm run typecheck
Professional workflow:
| Command | Purpose |
|---|---|
npm run dev | Fast dev server — no type checking |
npm run typecheck | Type check only, no output files |
npm run build | Type check + production bundle |
The implicit any gotcha
A common source of confusion: you add a TypeScript error to your code but tsc
reports nothing. The most likely reason is an implicit any.
function logName(name) {
// no type annotation
console.log(name)
}
logName() // no error!
When a parameter has no type annotation, TypeScript defaults it to any.
any accepts everything, so logName() with zero arguments is considered valid.
Once you add the type, the error appears:
function logName(name: string) {
console.log(name)
}
logName()
// Error: Expected 1 arguments, but got 0.
With "strict": true in your tsconfig.app.json (which Vite sets by default),
TypeScript will also catch the untyped parameter itself:
function logName(name) {
// ^^^^
// Error: Parameter 'name' implicitly has an 'any' type.
}
If you're not seeing errors you expect, always check:
- Are you running against
tsconfig.app.json(not the root config)? - Is
"strict": trueset? - Does the parameter or variable have an explicit type?