Vite 09 ⚡ TypeScript Support — Transpilation without type-checking
Vite treats TypeScript as a first-class language, but it does not treat type-checking as part of the build pipeline. When a .ts file is imported, Vite strips the types and transforms the syntax into JavaScript. It does not verify that the types are correct. A function that returns a string where the signature promises a number compiles without complaint, and the error surfaces at runtime or in the IDE, not in the Vite console.
This chapter covers what Vite actually does with TypeScript, why the transpile-only boundary is deliberate, the isolatedModules constraints that follow from it, and the two patterns for adding type-checking back into the workflow: the IDE as the primary feedback loop, and a background tsc --noEmit process or plugin for CI and pre-commit checks.
Key point: Vite transpiles TypeScript with esbuild, which strips types per-file without cross-file analysis. It cannot perform type-checking because type-checking requires the full program. The isolatedModules flag must be enabled in tsconfig.json to catch the constructs that cannot be transpiled safely without type information.
Why Vite does not type-check
The speed problem. Type-checking a TypeScript project requires the compiler to load every file, resolve every import, and build a complete type graph before it can report a single error. This is the expensive part of tsc. Transpiling a single file—stripping types and lowering syntax—is comparatively cheap. Vite’s development server is fast because it does the cheap part and skips the expensive part. The Vue.js documentation states this explicitly for Vite-based setups: “the dev server and bundler only perform transpilation and do not perform type checking. This ensures that the Vite dev server remains extremely fast even when using TypeScript” .
The per-file problem. Vite’s transform pipeline operates on one module at a time. A transform is a function from source text to output text. Type-checking is not a per-module operation; it requires knowledge of every module that the current module imports, and every module that imports it. The Vue.js documentation explains why the webpack-era approach of ts-loader was flawed: “the type system needs knowledge of the entire module network to perform type checking. The transformation stage of a single module is simply not the right place for this task” . Vite avoids this by not attempting it.
The isolatedModules consequence. Because esbuild transpiles each file in isolation, some TypeScript constructs cannot be handled safely. A re-export of a type (export { SomeType } from './types') is ambiguous without type information: esbuild cannot tell whether SomeType is a value or a type, and it cannot check whether the re-export is valid. The isolatedModules compiler option makes TypeScript report these constructs as errors, which forces the developer to write code that can be transpiled per-file. Vite’s templates set this option, and the Vue.js documentation notes that verbatimModuleSyntax is a superset that achieves the same goal .
a. What Vite does with TypeScript
Vite handles .ts and .tsx files out of the box. No plugin is required. The transform strips type annotations, interfaces, type aliases, and other type-level constructs, and leaves the runtime code. JSX in .tsx files is also handled by esbuild .
// input.ts
interface User {
id: string
name: string
}
export function greet(user: User): string {
return `Hello, ${user.name}`
}
// output.js (approximately)
export function greet(user) {
return `Hello, ${user.name}`
}
The output has no trace of the User interface. The type annotation on user is gone. The return type annotation is gone. The function body is unchanged. If the function body accessed a property that does not exist on User, Vite would not know. The IDE would flag it, but the build would succeed.
The tsconfig.json affects Vite’s behavior only in specific ways. The isolatedModules option is required. The paths option can be used for import resolution if resolve.tsconfigPaths is enabled, though the TypeScript team discourages relying on this for tool behavior . Other compiler options that affect emit—like target and useDefineForClassFields—are controlled by Vite’s esbuild or oxc configuration, not by tsconfig.json.
b. Client types and the Vite environment
Vite injects types for its own APIs—import.meta.env, import.meta.hot, and asset imports—through a vite/client type definition. Adding it to tsconfig.json is what makes those APIs visible to the IDE.
// tsconfig.json
{
"compilerOptions": {
"types": ["vite/client"]
}
}
Alternatively, a vite-env.d.ts file with a triple-slash reference achieves the same result . Without this, import.meta.env.VITE_API_URL is a type error in the editor, even though Vite resolves it at build time.
c. Adding type-checking back
The IDE provides type-checking as you type. That is the primary feedback loop during development, and it is sufficient for the developer who is actively editing the file. The gap is CI and pre-commit: a pull request can contain type errors that the IDE would have flagged, because the CI pipeline only runs vite build, which transpiles without checking.
The standard fix is a tsc --noEmit step. It runs the TypeScript compiler over the project without emitting output, and it fails if any type error exists.
// package.json
{
"scripts": {
"build": "tsc --noEmit && vite build",
"typecheck": "tsc --noEmit"
}
}
The tsc --noEmit command does not conflict with Vite. It reads the source files, builds the type graph, and reports errors. Vite then transpiles and bundles the same source files. The two tools operate on the same input but produce different outputs: tsc produces diagnostics, Vite produces JavaScript.
A plugin can run the type-checker in a separate process alongside the dev server. vite-plugin-checker is the commonly used option; it supports TypeScript, ESLint, Stylelint, and vue-tsc in a worker thread, so the dev server is not blocked . The @wroud/vite-plugin-tsc plugin provides a similar capability with explicit control over whether it runs in watch mode or only on build .
Complete Example Session
// ============================================
// PART 1: THE TSCONFIG SETUP
// ============================================
// tsconfig.json
{
"compilerOptions": {
"target": "ES2022",
"module": "ESNext",
"moduleResolution": "bundler",
"isolatedModules": true,
"types": ["vite/client"]
}
}
// isolatedModules is required for Vite's per-file transpilation.
// vite/client provides the import.meta.env and import.meta.hot types.
// ============================================
// PART 2: A TYPE ERROR THAT VITE IGNORES
// ============================================
// math.ts
export function add(a: number, b: number): number {
return a + b
}
// app.ts
import { add } from './math'
const result = add("1", 2) // type error: string is not assignable to number
// Vite transpiles both files without complaint.
// The IDE and tsc report the error.
// ============================================
// PART 3: ADDING TSC TO THE BUILD
// ============================================
// package.json
{
"scripts": {
"build": "tsc --noEmit && vite build",
"typecheck": "tsc --noEmit"
}
}
# ============================================
# PART 4: RUNNING THE CHECK
# ============================================
npm run typecheck
# Output:
# app.ts:4:18 - error TS2345: Argument of type 'string' is not
# assignable to parameter of type 'number'.
#
# Found 1 error.
# ============================================
# PART 5: THE VITE BUILD SUCCEEDS ANYWAY
# ============================================
npx vite build
# Output:
# vite v7.x.x building for production...
# ✓ built in 1.2s
#
# Vite does not run tsc.
# The build succeeds despite the type error.
// ============================================
// PART 6: ISOLATEDMODULES ERRORS
// ============================================
// types.ts
export interface User { id: string }
// index.ts
export { User } from './types'
// With isolatedModules: true, this is an error:
// "Re-exporting a type when 'isolatedModules' is enabled
// requires using 'export type'."
// Correct:
export type { User } from './types'
// ============================================
// PART 7: THE VITE CLIENT TYPES
// ============================================
// tsconfig.json
{
"compilerOptions": {
"types": ["vite/client"]
}
}
// Now import.meta.env is typed.
const apiUrl = import.meta.env.VITE_API_URL
// ============================================
// PART 8: A WATCH-MODE CHECKER
// ============================================
// vite.config.ts
import { defineConfig } from 'vite'
import checker from 'vite-plugin-checker'
export default defineConfig({
plugins: [
checker({ typescript: true }),
],
})
// Type errors appear in the terminal and the browser overlay.
// The dev server is not blocked.
// ============================================
// PART 9: THE PLUGIN ALTERNATIVE
// ============================================
import { tscPlugin } from '@wroud/vite-plugin-tsc'
export default defineConfig({
plugins: [
tscPlugin({
tscArgs: ['--project', 'tsconfig.json', '--noEmit'],
prebuild: false,
enableOverlay: true,
}),
],
})
// Type-checking only.
// The dev server starts immediately.
# ============================================
# PART 10: THE CI PIPELINE
# ============================================
# .github/workflows/ci.yml
# - run: npm ci
# - run: npm run typecheck
# - run: npm run build
#
# typecheck fails the pipeline if there are type errors.
# build produces the bundle.
The ten parts covered the tsconfig setup, a type error Vite ignores, adding tsc to the build, running the check, the Vite build succeeding, isolatedModules re-export errors, client types, a watch-mode checker, the plugin alternative, and the CI pipeline.
Quick Reference
What Vite does and does not do
| Operation | Vite | tsc |
|---|---|---|
| Strip types | Yes (esbuild) | Yes |
| Transpile syntax | Yes | Yes |
| Type-check | No | Yes |
| Resolve cross-file types | No | Yes |
| Emit JavaScript | Yes (in memory) | Yes (with --noEmit, no) |
Required tsconfig options
| Option | Value | Why |
|---|---|---|
isolatedModules | true | Catch constructs that cannot be transpiled per-file |
types | ["vite/client"] | Type import.meta.env and import.meta.hot |
Type-checking commands
| Command | Purpose |
|---|---|
tsc --noEmit | Check types, emit nothing |
tsc --noEmit --watch | Check types in watch mode |
vue-tsc --noEmit | Check types in Vue SFCs |
vite-plugin-checker | Run checks in a worker alongside Vite |
Client types
| API | Type source |
|---|---|
import.meta.env | vite/client |
import.meta.hot | vite/client |
Asset imports (.svg, .png) | vite/client |
Best Practices
✅ Do This:
// Enable isolatedModules in tsconfig
"isolatedModules": true // ✅
// Add tsc --noEmit to the build script
"build": "tsc --noEmit && vite build" // ✅
// Include vite/client types
"types": ["vite/client"] // ✅
// Use a checker plugin for dev feedback
checker({ typescript: true }) // ✅
❌ Don’t Do This:
// Rely on vite build for type-checking
"build": "vite build" // no tsc // ❌
// Re-export a type without export type
export { User } from './types' // isolatedModules error // ❌
// Skip isolatedModules
// Vite templates enable it for a reason // ❌
// Assume Vite catches type errors
const x: number = "string" // Vite ignores it // ❌
Common Pitfalls
| Pitfall | Why It Happens | Fix |
|---|---|---|
| Type errors reach production | vite build does not run tsc | Add tsc --noEmit to the build script |
import.meta.env is a type error | Missing vite/client types | Add to tsconfig.json types |
Re-export error under isolatedModules | Type re-exported as value | Use export type { ... } |
| Slow IDE feedback | The IDE is the only checker | The IDE is the primary feedback; add a CI check |
| Build fails in CI but not locally | Local build skipped tsc | Use the same build script in both environments |
Real-World Examples
1. Type-check command
{ "scripts": { "typecheck": "tsc --noEmit" } }
2. Build with type-check
{ "scripts": { "build": "tsc --noEmit && vite build" } }
3. isolatedModules
{ "compilerOptions": { "isolatedModules": true } }
4. Client types
{ "compilerOptions": { "types": ["vite/client"] } }
5. Type-only re-export
export type { User } from './types'
6. Checker plugin
checker({ typescript: true })
7. Watch mode
tsc --noEmit --watch
8. Vue SFC check
vue-tsc --noEmit
9. CI step
- run: npm run typecheck
10. Vite build alone
npx vite build # transpiles, does not check
Visual
The transpile-only boundary
┌─────────────────────────────────────────────────────────────┐
│ VITE TRANSFORM PIPELINE │
│ │
│ source.ts │
│ │ │
│ ▼ │
│ esbuild transform │
│ ├── strip type annotations │
│ ├── strip interfaces │
│ ├── strip type aliases │
│ └── lower syntax │
│ │ │
│ ▼ │
│ output.js │
│ │
│ No type-checking at any step. │
│ A type error in source is a syntax error in output. │
└─────────────────────────────────────────────────────────────┘
The two feedback loops
┌─────────────────────────────────────────────────────────────┐
│ DEVELOPMENT CI │
│ │
│ IDE tsc --noEmit │
│ ├── type errors as you type ├── full program check │
│ ├── instant feedback ├── fails the pipeline │
│ └── no build step └── runs before build │
│ │
│ Vite dev server Vite build │
│ ├── transpiles on demand ├── transpiles │
│ ├── HMR ├── bundles │
│ └── no type-checking └── no type-checking │
└─────────────────────────────────────────────────────────────┘
isolatedModules constraints
┌─────────────────────────────────────────────────────────────┐
│ ALLOWED NOT ALLOWED │
│ │
│ export type { User } export { User } │
│ export interface Foo export { Foo } │
│ import type { User } import { User } │
│ │
│ The `type` keyword makes the │
│ type/value distinction explicit. │
└─────────────────────────────────────────────────────────────┘
The CI pipeline
┌─────────────────────────────────────────────────────────────┐
│ CI │
│ │
│ npm ci │
│ │ │
│ ▼ │
│ tsc --noEmit ──► type errors ──► FAIL │
│ │ │
│ └── no errors ──► vite build ──► dist/ │
│ │
│ The build cannot produce output │
│ if the type-check fails. │
└─────────────────────────────────────────────────────────────┘
Summary
| Item | Value |
|---|---|
| Transpiler | esbuild |
| Type-checking | Not performed by Vite |
| Required tsconfig option | isolatedModules: true |
| Client types | "types": ["vite/client"] |
| Type-check command | tsc --noEmit |
| CI integration | "build": "tsc --noEmit && vite build" |
| Dev checker plugin | vite-plugin-checker |
| Vue SFC checker | vue-tsc --noEmit |
Key takeaways:
- Vite transpiles TypeScript; it does not type-check. The esbuild transform strips types and lowers syntax per file. It cannot perform type-checking because type-checking requires the full program, not a single module .
isolatedModulesis required for correctness. Because each file is transpiled without knowledge of the others, TypeScript constructs that depend on cross-file type information must be written explicitly.export typeandimport typeare the patterns that make the per-file model safe .- The IDE is the primary feedback loop. Type errors appear in the editor as the developer types. This is the fastest feedback and the one that catches most errors before they are committed.
- The CI pipeline must add
tsc --noEmit.vite buildalone will succeed even when the project contains type errors. A build script that runstsc --noEmit && vite buildensures that the bundle is not produced from code that fails type-checking. vite/clientprovides the types for Vite’s own APIs. Without it,import.meta.envandimport.meta.hotare type errors in the editor, even though they work at runtime .- A checker plugin can run type-checking alongside the dev server.
vite-plugin-checkerand similar tools run the checker in a worker thread, so the dev server remains fast while type errors appear in the terminal and the browser overlay .
Remember: Vite’s speed comes from doing less. It transpiles TypeScript without checking it, which means the dev server starts instantly and the HMR updates apply in milliseconds. The trade-off is that type errors are invisible to the build. The fix is not to make Vite check types—that would negate the speed advantage. The fix is to use the IDE for immediate feedback and add tsc --noEmit to the CI pipeline for the final gate. The isolatedModules flag is what makes the per-file transpilation safe by forcing the type/value distinction to be explicit.
Stop using slow, ad-bloated tool sites! 🤮
🔎 Search “KandZ Tools” on Google to use many professional utilities for free.
KandZ.me is the ultimate minimalist hub for:
✅ Finance (Mortgage, Interest, Inflation)
✅ Tech (Base64, JSON, Dev Suite, IP)
✅ Health (BMI, BMR, TDEE)
✅ Productivity (Timer, Workspace, QR)
⚡️ Fast & Private
🔒 No data leaves your device
💎 100% Free
🔗 Use it now: https://tools.kandz.me
🔖 Bookmark it—you’ll need it later!