34.5 Vite: TypeScript Support Out of the Box
Right, so you’ve written some TypeScript. Congratulations. Now, how do you get it from a collection of fancy .ts files into something a browser can actually understand without throwing a tantrum? You could wrestle with a complex Webpack config for an afternoon (and I have the grey hairs to prove it), or you could use Vite and have it working in about 30 seconds. Vite is the “just works” tool we’ve been waiting for, and its TypeScript support is so seamless you might forget you’re not just writing JavaScript.
Here’s the beautiful part: Vite treats TypeScript as a first-class citizen. You don’t install a special plugin. You don’t need to configure a loader. You just… use TypeScript. Vite uses esbuild under the hood to transpile your TypeScript into JavaScript. Esbuild is written in Go and is brutally, absurdly fast. We’re talking “blink and you’ll miss it” fast. This speed is the core of Vite’s magical development experience.
The Bare Minimum to Get Going
Your vite.config.ts (see, it even lets you write its own config in TypeScript, which is a nice touch) can be utterly simple. This is all you need to handle your .ts and .tsx files:
import { defineConfig } from 'vite';
export default defineConfig({
// That's it. No, seriously.
});
Vite’s defaults are brilliantly sane. It will automatically look for a tsconfig.json in your project root and use it. It assumes you want your source in a directory called src, and it will handle the transpilation, serving, and hot module reloading (HMR) without any further input. Just run vite or vite dev and you’re off.
Why vite.config.ts is a Power Move
Writing your config file in TypeScript isn’t just for showing off. It gives you autocomplete and type checking for your configuration, which is a fantastic way to avoid silly typos and discover advanced options. Notice we’re using defineConfig—this is a helper function that provides that sweet, sweet IntelliSense. Try typing server: { p and watch your editor suggest port. It’s a small thing that prevents a lot of frustration.
When Vite Defers to Your tsconfig.json
It’s crucial to understand the division of labor. Vite (via esbuild) is only responsible for transpilation—converting TypeScript syntax to JavaScript and stripping types. It does not perform type checking. That’s the job of your IDE or the TypeScript compiler (tsc) itself.
This means all your compiler options that affect runtime behavior—like target, module, lib, jsx, paths for aliases—are respected by Vite. However, options that are purely about the type-checking process, like noEmit or strict, are only used by your IDE and tsc. Vite ignores them because they don’t affect the output code. This is a good thing! It lets your editor scream about type errors while Vite happily bundles your (theoretically flawed) application.
The One “Gotcha”: Type-Only Imports
Here’s a common pitfall that trips people up. Let’s say you have a type you want to import:
// types.ts
export interface User {
id: number;
name: string;
}
You might try to import it in another file:
// main.ts
import { User } from './types';
const newUser: User = { id: 1, name: 'Sarah' };
This works. But esbuild, in its quest for maximum speed, performs tree-shaking at the source level. If it determines that User is only used as a type and never as a value, it will see the import as unnecessary and remove it. This is usually what you want. The problem arises with import elision.
Now, consider a different approach where you use a value and a type from the same module:
// utils.ts
export function createUser(name: string) {
return { id: Math.random(), name };
}
export interface User {
id: number;
name: string;
}
If you try to import both in a naive way, you might get an error:
// main.ts
import { createUser, User } from './utils'; // <-- This import might be entirely elided!
const newUser: User = createUser('Sarah');
The solution is to use TypeScript’s explicit type-only import syntax. This clearly signals to the transpiler what should be removed and what should be kept.
// main.ts
import { createUser } from './utils';
import type { User } from './utils'; // <-- Explicitly a type import
const newUser: User = createUser('Sarah');
This is a best practice anyway, as it makes your code’s intent crystal clear. Vite and esbuild will handle this correctly every time.
The Path to Enlightenment (and Aliases)
A fantastic feature in TypeScript is path aliases in tsconfig.json to avoid hideous relative import paths like ../../../../utils.
// tsconfig.json
{
"compilerOptions": {
"baseUrl": ".",
"paths": {
"@/*": ["src/*"],
"@utils/*": ["src/utils/*"]
}
}
}
For this to work in Vite, you need to tell the Vite dev server and build bundler (Rollup) about these aliases. Luckily, it’s a one-line fix. First, install @types/node so you can use Node path module.
npm install -D @types/node
Then, update your vite.config.ts:
import { defineConfig } from 'vite';
import path from 'path';
export default defineConfig({
resolve: {
alias: {
'@': path.resolve(__dirname, './src'),
'@utils': path.resolve(__dirname, './src/utils'),
},
},
});
Now, you can write beautiful imports like import MyComponent from '@/components/MyComponent' and both TypeScript and Vite will know exactly what you’re talking about. It’s a small configuration for a massive quality-of-life improvement.