9.3 Bundler Integration — Vite, webpack, Rollup + TypeScript
Bundlers and TypeScript
Bundlers combine multiple modules into one (or several) files. Using them with TypeScript requires TS → JS transformation configuration.
| Bundler | TS Processing | Characteristics |
|---|---|---|
| Vite | esbuild (built-in) | Fast dev server, HMR |
| webpack | ts-loader / babel-loader | Mature ecosystem, complex config |
| Rollup | @rollup/plugin-typescript | Optimal for library bundling |
| Parcel | Built-in (auto-detect) | Zero config, beginner-friendly |
Vite + TypeScript
Creating a Project
npm create vite@latest my-app -- --template react-ts
# or Vue
npm create vite@latest my-app -- --template vue-ts
# or vanilla TS
npm create vite@latest my-app -- --template vanilla-ts
Generated tsconfig Structure
// tsconfig.json (for app code)
{
"compilerOptions": {
"target": "ES2020",
"useDefineForClassFields": true,
"lib": ["ES2020", "DOM", "DOM.Iterable"],
"module": "ESNext",
"skipLibCheck": true,
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"resolveJsonModule": true,
"isolatedModules": true,
"noEmit": true, // Vite handles bundling, tsc doesn't emit
"jsx": "react-jsx",
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noFallthroughCasesInSwitch": true
},
"include": ["src"],
"references": [{ "path": "./tsconfig.node.json" }]
}
// tsconfig.node.json (for vite.config.ts and Node.js tools)
{
"compilerOptions": {
"composite": true,
"skipLibCheck": true,
"module": "ESNext",
"moduleResolution": "bundler",
"allowSyntheticDefaultImports": true,
"strict": true
},
"include": ["vite.config.ts"]
}
Key vite.config.ts Settings
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
import path from 'path';
export default defineConfig({
plugins: [react()],
// Path aliases
resolve: {
alias: {
'@': path.resolve(__dirname, './src'),
},
},
// Build settings
build: {
target: 'es2020',
outDir: 'dist',
sourcemap: true,
rollupOptions: {
output: {
manualChunks: {
vendor: ['react', 'react-dom'],
router: ['react-router-dom'],
},
},
},
},
// Environment variable prefix
envPrefix: 'VITE_',
});
Environment Variable Type Definitions in Vite
// src/vite-env.d.ts
/// <reference types="vite/client" />
interface ImportMetaEnv {
readonly VITE_API_URL: string;
readonly VITE_APP_TITLE: string;
}
interface ImportMeta {
readonly env: ImportMetaEnv;
}
// Usage
const apiUrl = import.meta.env.VITE_API_URL; // string
webpack + TypeScript
Installation
npm install --save-dev webpack webpack-cli ts-loader typescript
webpack.config.ts
import path from 'path';
import webpack from 'webpack';
import HtmlWebpackPlugin from 'html-webpack-plugin';
const config: webpack.Configuration = {
entry: './src/index.tsx',
output: {
path: path.resolve(__dirname, 'dist'),
filename: '[name].[contenthash].js',
clean: true,
},
resolve: {
extensions: ['.tsx', '.ts', '.js'],
alias: {
'@': path.resolve(__dirname, 'src'),
},
},
module: {
rules: [
{
test: /\.(ts|tsx)$/,
use: 'ts-loader',
exclude: /node_modules/,
},
{
test: /\.css$/,
use: ['style-loader', 'css-loader'],
},
],
},
plugins: [
new HtmlWebpackPlugin({
template: './public/index.html',
}),
],
optimization: {
splitChunks: {
chunks: 'all',
},
},
};
export default config;
ts-loader vs babel-loader Comparison
ts-loader (recommended: includes type checking):
{
test: /\.tsx?$/,
use: 'ts-loader',
exclude: /node_modules/,
}
babel-loader + @babel/preset-typescript (no type checking, faster):
npm install --save-dev babel-loader @babel/core @babel/preset-typescript @babel/preset-env
{
test: /\.tsx?$/,
use: {
loader: 'babel-loader',
options: {
presets: [
'@babel/preset-env',
'@babel/preset-typescript',
['@babel/preset-react', { runtime: 'automatic' }],
],
},
},
}
Optimizing ts-loader
{
test: /\.tsx?$/,
use: [
{
loader: 'ts-loader',
options: {
transpileOnly: true, // Skip type checking (faster)
// Type checking handled by fork-ts-checker-webpack-plugin
},
},
],
}
npm install --save-dev fork-ts-checker-webpack-plugin
import ForkTsCheckerWebpackPlugin from 'fork-ts-checker-webpack-plugin';
{
plugins: [
new ForkTsCheckerWebpackPlugin(), // Type check in separate process
],
}
Rollup + TypeScript
Ideal for library publishing. Excellent tree-shaking.
Installation
npm install --save-dev rollup @rollup/plugin-typescript rollup-plugin-dts
rollup.config.ts
import { defineConfig } from 'rollup';
import typescript from '@rollup/plugin-typescript';
import dts from 'rollup-plugin-dts';
import resolve from '@rollup/plugin-node-resolve';
import commonjs from '@rollup/plugin-commonjs';
export default defineConfig([
// Main build (CJS + ESM simultaneous output)
{
input: 'src/index.ts',
external: ['react', 'react-dom'], // Exclude peer dependencies
output: [
{
file: 'dist/index.cjs',
format: 'cjs',
sourcemap: true,
},
{
file: 'dist/index.mjs',
format: 'es',
sourcemap: true,
},
],
plugins: [
resolve(),
commonjs(),
typescript({ tsconfig: './tsconfig.json' }),
],
},
// Bundle type declaration files
{
input: 'dist/types/index.d.ts',
output: [{ file: 'dist/index.d.ts', format: 'es' }],
plugins: [dts()],
},
]);
package.json Library Configuration
{
"name": "my-library",
"version": "1.0.0",
"main": "./dist/index.cjs",
"module": "./dist/index.mjs",
"types": "./dist/index.d.ts",
"exports": {
".": {
"import": "./dist/index.mjs",
"require": "./dist/index.cjs",
"types": "./dist/index.d.ts"
}
},
"scripts": {
"build": "rollup -c rollup.config.ts --configPlugin typescript",
"typecheck": "tsc --noEmit"
},
"peerDependencies": {
"react": ">=18"
}
}
Parcel + TypeScript
TypeScript works without any configuration.
npm install --save-dev parcel
{
"scripts": {
"dev": "parcel src/index.html",
"build": "parcel build src/index.html"
}
}
Parcel automatically detects tsconfig.json. Best for small projects or prototyping.
The isolatedModules Option
An important option when using bundlers.
{
"compilerOptions": {
"isolatedModules": true
}
}
Forces each file to be transpilable independently. esbuild, swc, and Babel process files individually, so this option is needed.
// Patterns that violate isolatedModules
// ❌ const enum cannot be inlined from other files
const enum Direction { Up, Down } // Error
// ❌ Type re-exports need 'export type'
export { SomeType }; // Error
export type { SomeType }; // ✅
// ❌ Namespaces must contain only types
namespace MyNs {
export const value = 1; // Error (contains runtime value)
}
Common Bundle Optimization Strategies
Enabling Tree-shaking
// ✅ Named exports (tree-shakable)
export function add(a: number, b: number) { return a + b; }
export function subtract(a: number, b: number) { return a - b; }
// ❌ Default export object (harder to tree-shake)
export default { add, subtract };
Code Splitting with Dynamic Imports
// Load on demand (lazy loading)
const { PDFViewer } = await import('./components/PDFViewer');
// In React
const LazyComponent = React.lazy(() => import('./HeavyComponent'));
Pro Tips
Bundler selection guide
New React/Vue project → Vite (fastest dev experience)
Legacy CRA project → webpack (maintain existing config)
Library publishing → Rollup (best tree-shaking)
Server-side Node.js → esbuild directly (fastest)
Prototype/small project → Parcel (zero config)
Separation of tsc and bundler responsibilities
tsc: Type checking only (noEmit: true)
Bundler: Transpilation + bundling + optimization