Using Rollup with TypeScript: An Intermediate Guide
Introduction
Rollup is a powerful JavaScript module bundler that excels at producing small, efficient bundles for libraries and applications. When combined with TypeScript, Rollup gives you the benefits of static typing and modern build outputs, but the integration can be tricky: you need to manage compilation, declaration files, tree-shaking, code splitting, and plugin configurations for optimal performance and DX. This guide targets intermediate developers who are already comfortable with TypeScript and modern JavaScript tooling but want a practical, in-depth walkthrough for using Rollup as the central bundler in their projects.
In this article you'll learn how to set up Rollup with TypeScript, configure plugins for common targets (ESM, CJS, UMD), generate type declarations, and adopt advanced techniques like incremental builds, caching, and performance tuning. We'll cover practical examples, step-by-step configs, and troubleshooting tips so you can ship smaller, faster bundles that play well with downstream consumers and monorepos. Along the way I'll reference related concepts—like TypeScript compile-time performance, declaration file strategies, and code organization—to help you integrate Rollup into larger codebases predictably.
By the end you'll be able to: create a Rollup build pipeline for a library or app, emit type declarations, optimize bundle output for consumers, and apply advanced strategies to keep builds fast and maintainable. If you want concrete implementation patterns—plugin setups, sample rollup.config.js files, and CLI scripts—this guide provides them with explanations tailored for intermediate developers.
Background & Context
Rollup focuses on ES module semantics and excels at tree-shaking and producing flat, minimal bundles—an ideal fit for libraries and modern web apps. TypeScript introduces types and sometimes introduces extra build steps (emit, declaration files) that bundlers must account for. Unlike bundlers designed for apps (like Webpack or Vite), Rollup's core API and rich plugin ecosystem let you craft optimized library outputs (multiple formats, careful externalization, and fine-grained treeshake controls).
Combining Rollup and TypeScript means balancing TypeScript's compiler (tsc) responsibilities and Rollup's bundling responsibilities. You can let tsc emit JavaScript and feed that to Rollup, or use plugins to compile TypeScript during bundling. Each approach has trade-offs for build speed, source maps, and declaration generation.
This guide explains options, shows realistic configs, and highlights when to use tsc vs plugin compilation. We'll also touch on related concerns like maintaining fast TypeScript compile times and organizing types for monorepos so your Rollup pipeline scales.
Key Takeaways
- Choose the right TypeScript + Rollup workflow (tsc-first vs plugin-first).
- Configure Rollup outputs for multiple formats: ESM, CJS, and UMD.
- Generate and publish accurate .d.ts declaration files.
- Externalize dependencies and optimize treeshaking for smaller bundles.
- Use caching, incremental builds, and watch mode to speed dev cycles.
- Common pitfalls and troubleshooting steps for source maps, path aliases, and plugin order.
Prerequisites & Setup
Before diving in, make sure you have the following installed and basic knowledge:
- Node.js (14+ recommended) and npm/yarn/pnpm.
- TypeScript (3.8+ recommended) and familiarity with tsconfig.json.
- Basic understanding of Rollup and JavaScript module formats (ESM, CJS).
- A modern code editor (VSCode recommended) and familiarity with npm scripts.
Create a new project or use an existing TypeScript codebase. Install Rollup and recommended plugins:
npm install --save-dev rollup typescript @rollup/plugin-node-resolve @rollup/plugin-commonjs @rollup/plugin-json rollup-plugin-typescript2 rollup-plugin-terser
We're using rollup-plugin-typescript2 for robust TypeScript integration (it supports caching and declarationEmit control). You'll also optionally install terser for minification.
Main Tutorial Sections
1) Choosing a Build Strategy: tsc-first vs plugin-first
Two main approaches exist for TypeScript + Rollup:
- tsc-first: run tsc to emit JS and .d.ts files, then bundle the emitted JS with Rollup. Good for faster CI where you want a trusted tsc step and separate declaration generation.
- plugin-first: use a Rollup TypeScript plugin (like rollup-plugin-typescript2) to compile TypeScript during bundling, often faster to configure and supports single-pass builds with sourcemaps.
Recommendation: For libraries that must ship precise .d.ts, consider tsc-first for declaration control and plugin-first for simpler setups or apps where single-step builds are preferred. If you're curious about compile performance trade-offs, read our guide on Performance Considerations: TypeScript Compilation Speed.
2) Basic rollup.config.js for a Library
Here's a minimal config emitting ESM and CJS with types compiled by rollup-plugin-typescript2:
import resolve from '@rollup/plugin-node-resolve';
import commonjs from '@rollup/plugin-commonjs';
import typescript from 'rollup-plugin-typescript2';
import { terser } from 'rollup-plugin-terser';
export default [{
input: 'src/index.ts',
external: ['lodash'], // keep large deps external
plugins: [resolve(), commonjs(), typescript({ useTsconfigDeclarationDir: true }), terser()],
output: [
{ file: 'dist/index.esm.js', format: 'es', sourcemap: true },
{ file: 'dist/index.cjs.js', format: 'cjs', sourcemap: true }
]
}];Key points: externalize peer deps, use sourcemaps, and let typescript plugin emit declarations into tsconfig-specified folder.
3) Publishing Multiple Formats and package.json Fields
For libraries, publish ESM and CJS. In package.json:
{
"main": "dist/index.cjs.js",
"module": "dist/index.esm.js",
"types": "dist/index.d.ts",
"exports": {
".": {
"import": "dist/index.esm.js",
"require": "dist/index.cjs.js"
}
}
}The types field must point to your generated declaration file. When using rollup-plugin-typescript2 or tsc-first, ensure the declaration path matches.
4) Generating Declaration Files (.d.ts)
Accurate declaration files are critical for library consumers. Two common approaches:
- Let tsc emit declarations: set "declaration": true and "declarationDir" in tsconfig.json and run
tsc --emitDeclarationOnly. - Use rollup-plugin-typescript2 which can emit declarations during bundle. Note: plugin d.ts emission can be less flexible for complex monorepos.
Example tsconfig for declaration-only step:
{
"compilerOptions": {
"declaration": true,
"emitDeclarationOnly": true,
"declarationDir": "dist/types",
"module": "esnext",
"target": "es2019",
"skipLibCheck": true
},
"include": ["src"]
}Run tsc --project tsconfig.json before Rollup to ensure types are generated. For strategies when you need to write custom declarations (e.g., wrapping untyped JS), see our guide on Writing Declaration Files for Complex JavaScript Libraries.
5) Path Aliases and Module Resolution
Many TypeScript projects use path aliases in tsconfig (e.g., "@utils/*"). Rollup needs to resolve those at build time. Use plugin-alias or configure module-resolver transformations.
Example with @rollup/plugin-alias:
import alias from '@rollup/plugin-alias';
plugins: [
alias({ entries: [{ find: '@utils', replacement: './src/utils' }] }),
resolve(), commonjs(), typescript()
]Also ensure tsconfig's "paths" mirror the alias mapping. If you run tsc-first, make sure tsc's output path mapping keeps imports resolvable for Rollup.
6) Externalizing Dependencies and Peer Dependencies
To keep bundles small and avoid bundling common libs like React or lodash, mark them as external in Rollup and list them as peerDependencies in package.json.
external: id => /^(react|lodash)/.test(id)
This preserves tree-shaking in consumer apps and avoids duplicate copies. For monorepos or shared types, see patterns in Managing Types in a Monorepo with TypeScript.
7) Source Maps and Debugging
Enable source maps both in TypeScript and Rollup for accurate stack traces. With plugin-first, pass tsconfigOverride and ensure sourceMap: true in plugin options.
Example:
typescript({ tsconfigOverride: { compilerOptions: { sourceMap: true } } })If you use tsc-first, generate source maps with tsc and let Rollup consume them. Troubleshoot mismatched lines by checking inlineSources and sourceRoot settings in tsconfig.
8) Tree-shaking and Side Effects
Rollup performs tree-shaking using ES module semantics. Ensure your package.json includes "sideEffects": false when modules are pure to allow more aggressive elimination.
If some files run initialization code, list them explicitly:
"sideEffects": ["./src/some-global-init.js"]
Tree-shaking behavior is also affected by how you export values; prefer named exports for fine-grained elimination.
9) Build Scripts, Watch Mode, and Incremental Builds
For a good dev experience, combine watch mode and caching. rollup-plugin-typescript2 has an in-memory cache that speeds repeated builds. Use rollup's watch API or the CLI:
rollup -c -w
For faster rebuilds, separate type emission from bundling in dev: run tsc --watch --emitDeclarationOnly in parallel if you need types checkers running, and use Rollup's watch for bundles. For guidance on speeding TypeScript compilation broadly, see Performance Considerations: TypeScript Compilation Speed.
10) Integrating Linters and Prettier into the Build
Linting improves code quality and prevents runtime issues prior to bundling. Integrate ESLint with TypeScript-specific rules and run it as a pre-build step. See our practical guide to Integrating ESLint with TypeScript Projects (Specific Rules) for recommended rules and automation.
Automate linting in CI and include autofix targets in local scripts so your Rollup builds consume well-formed source.
Advanced Techniques
Once you have a stable build, consider these expert optimizations:
- Differential builds: output modern ESM for modern browsers and legacy CJS/UMD for older environments. This can be achieved by multiple Rollup configs or using conditional builds in CI.
- Persistent caching: use rollup-plugin-typescript2's cache directory or implement a file-system cache for expensive transforms.
- Code-splitting and dynamic imports: use dynamic import() to produce multiple chunks and let Rollup produce optimal output for lazy-loaded modules.
- Minify selectively: only minify production outputs and keep readable development bundles; use terser with mangle options tuned for library consumers.
- Analyze bundles with rollup-plugin-visualizer to uncover large dependencies and duplication.
Performance tuning interacts with TypeScript runtime concerns—understand where type-checking is expensive and offload non-critical checks to CI; for runtime overhead discussions, see Performance Considerations: Runtime Overhead of TypeScript (Minimal).
Best Practices & Common Pitfalls
Dos:
- Do externalize peer dependencies to avoid bundling duplicates.
- Do generate and verify declaration files before publishing.
- Do set "sideEffects" in package.json correctly to enable tree-shaking.
- Do use consistent source map settings between tsc and Rollup.
Don'ts:
- Don't rely on rollup-plugin-typescript2 to always produce perfect declaration layouts for complex monorepos; use a tsc-first step when needed.
- Don't forget to update package.json "types" and "exports" when changing output layout.
- Don't bundle native Node modules (fs, path) for browser builds—mark them as external.
Troubleshooting tips:
- If types are missing for consumers, confirm that .d.ts files are included in your npm package (inspect the packed tarball with
npm pack). - For unexpected large bundles, run a visualizer and check for transitive dependencies; make sure you externalized large libs.
- If sourcemaps show wrong lines, enable inlineSources and check sourceRoot settings in tsconfig.
For organizing code to make bundling predictable, see our recommendations in Code Organization Patterns for TypeScript Applications.
Real-World Applications
Rollup + TypeScript is an excellent combination for:
- Libraries meant for npm distribution (small, tree-shakeable packages).
- Utility packages that expose multiple entry points and need accurate declaration files.
- Shared UI component libraries when you need to ship ESM modules consumed by different bundlers.
Case example: a UI component library would use Rollup to output ESM (for modern bundlers), CJS (for Node consumers), emit .d.ts for each component, and externalize React and styled-system. For monorepos, center type sharing using patterns from Managing Types in a Monorepo with TypeScript.
Another practical example: bundling a library that interacts with a database client — carefully type client interactions and ensure declarations are published so downstream apps get type-safety; review patterns at Typing Database Client Interactions in TypeScript.
If your project involves workers (Web or Service Workers), Rollup can be configured to bundle worker entry points separately. For more on typing and bundling workers in TypeScript, check the guides on Using TypeScript with Web Workers: A Comprehensive Guide for Intermediate Developers and Using TypeScript with Service Workers: A Practical Guide.
Conclusion & Next Steps
Rollup with TypeScript gives you a flexible, high-performance build pipeline for libraries and apps. Start by choosing the right compilation strategy (tsc-first or plugin-first), ensure declarations are correctly generated, and configure outputs for the formats your consumers need. Optimize bundles by externalizing dependencies, enabling source maps, and using caching for faster rebuilds. Continue learning by exploring TypeScript compilation performance, code organization, and declaration authoring in the linked resources.
Next steps: set up a sample repo using the patterns in this guide, add CI to run type checks and bundle validation, and iterate on bundle size with visualizer tools.
Enhanced FAQ
Q: Should I run tsc before Rollup or rely on a Rollup TypeScript plugin?
A: Both approaches are valid. tsc-first (running tsc --emitDeclarationOnly or full tsc) gives you precise control over declaration file generation and separates concerns. It's beneficial for large codebases and CI where you want a separate type-checking stage. The plugin-first approach (e.g., rollup-plugin-typescript2) simplifies local development by compiling on the fly, offering caching and single-step builds. For complex monorepos or strict declaration layout needs, prefer tsc-first. For simpler libraries or apps, plugin-first reduces maintenance. See performance implications in Performance Considerations: TypeScript Compilation Speed.
Q: How do I ensure correct .d.ts files are published?
A: Use tsc to emit declarations into a predictable directory (set declarationDir), add that directory to the published files in package.json (or ensure npm includes it), and set the types field in package.json to point to the main d.ts entry. Validate with npm pack and inspect the tarball to confirm declarations are present. For complex or handwritten declarations, consult Writing Declaration Files for Complex JavaScript Libraries.
Q: How do I debug source map mismatches between TypeScript and Rollup?
A: Confirm both tsc and Rollup are configured to generate source maps. If you use plugin-first, enable sourceMap: true in plugin options and inlineSources in tsconfig for more accurate traces. If using tsc-first, ensure tsc emits source maps and Rollup consumes them (Rollup will try to read the input source maps by default). Mismatched file paths often come from path aliases—make sure alias mappings are consistent between tsconfig and Rollup.
Q: Can Rollup handle code-splitting for libraries?
A: Yes—Rollup supports code-splitting via multiple entry points or dynamic import(). For libraries, multiple entry points can produce individual bundles for subpaths (e.g., import { button } from 'ui-lib/button'). Set input in rollup.config to an object mapping entries and use output.dir to get multiple files. Note that code-splitting interacts with package exports, so update exports in package.json accordingly.
Q: How should I manage dependencies vs peerDependencies?
A: Dependencies required at runtime by your package should go into dependencies, while packages expected to be provided by the consumer (like React) should be peerDependencies. Mark peerDependencies as external in Rollup (via external) so they are not bundled. This avoids multiple React copies and reduces bundle size.
Q: What about monorepos and shared types?
A: In monorepos, centralize type declarations when possible and reference them via project references or via a shared types package. When bundling with Rollup, ensure the build resolves cross-package imports—either by externalizing them or using path mappings and prebuilt outputs. For strategies and examples, see Managing Types in a Monorepo with TypeScript.
Q: How can I minimize bundle size further?
A: Analyze your bundle with visualizer plugins, externalize large libraries, prefer ESM builds that allow consumer bundlers to treeshake, and avoid importing entire libraries (use named imports where possible). Also consider runtime alternatives (smaller utility libraries) and use terser with conservative mangle settings for libs. Review additional runtime considerations in Performance Considerations: Runtime Overhead of TypeScript (Minimal).
Q: How does bundling differ for apps vs libraries?
A: Apps typically bundle all dependencies for deployment and may require polyfills or runtime-only transforms. Libraries should remain small, externalize dependencies, and prioritize type correctness for consumers. Rollup is often chosen for libraries due to its ESM-first philosophy and efficient tree-shaking. If you're building CLI tools in TypeScript instead, check patterns in Building Command-Line Tools with TypeScript: An Intermediate Guide.
Q: Any tips for integrating workers and Rollup builds?
A: Treat workers (Web or Service Workers) as separate entry points in Rollup; bundle them separately and reference the emitted worker file from your main bundle via dynamic import or new Worker(). Also ensure worker code is typed and built to the right target—see guidance in Using TypeScript with Web Workers: A Comprehensive Guide for Intermediate Developers and Using TypeScript with Service Workers: A Practical Guide.
Q: Where should I go next to improve my pipeline?
A: Deepen your knowledge on code organization (see Code Organization Patterns for TypeScript Applications), learn about advanced typing patterns (e.g., for React or Redux if relevant), and refine linting and CI practices referencing Integrating ESLint with TypeScript Projects (Specific Rules). For runtime type integration with databases, see Typing Database Client Interactions in TypeScript.
