TypeScript config in a Vite + React project

Table of Contents

  1. Why there are two tsconfig files
  2. tsconfig.json — the root coordinator
  3. tsconfig.app.json — browser source code
  4. tsconfig.node.json — Vite config (Node.js)
  5. Running the type checker
  6. Why npx tsc shows no errors
  7. noEmit — why TypeScript doesn't output files
  8. VS Code checks types automatically
  9. tsc: command not found
  10. Watch mode and the typecheck script
  11. The implicit any gotcha

Why there are two tsconfig files

↑ Index

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:

FileWhat it checksEnvironment
tsconfig.app.jsonsrc/ — your React componentsBrowser (DOM)
tsconfig.node.jsonvite.config.tsNode.js

These two environments need different settings. For example:

  • Browser code needs "lib": ["DOM"] so TypeScript knows about document, window, etc.
  • Node.js code needs Node types and has no DOM globals.

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

↑ Index

{
  "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

↑ Index

{
  "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 the src/ 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 .js files (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)

↑ Index

{
  "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

↑ Index

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).

CommandModeWhat it does
npx tsc -p tsconfig.app.json --noEmitDirectChecks one config, no output
npx tsc --buildProject referencesChecks all referenced configs, uses cache
npx tscDirect (root)Checks root config — does nothing in Vite projects

Why npx tsc shows no errors

↑ Index

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

↑ Index

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

↑ Index

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):

  1. Press Cmd+Shift+P
  2. Type TypeScript: Select TypeScript Version
  3. Choose Use Workspace Version

This matters when your project uses a newer TypeScript than VS Code's bundled version.

tsc: command not found

↑ Index

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

↑ Index

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:

CommandPurpose
npm run devFast dev server — no type checking
npm run typecheckType check only, no output files
npm run buildType check + production bundle

The implicit any gotcha

↑ Index

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:

  1. Are you running against tsconfig.app.json (not the root config)?
  2. Is "strict": true set?
  3. Does the parameter or variable have an explicit type?