
Using tsup to bundle your TypeScript package
Using tsup to bundle your TypeScript package 관련

tsup is a fast and efficient, zero-configuration TypeScript bundler designed to streamline the process of compiling, optimizing, and outputting different module formats. Unlike older bundlers, tsup leverages esbuild under the hood for high-speed performance, supports modern ECMAScript modules and CommonJS(CJS), and provides built-in features like tree shaking, minification, and code splitting.

This guide walks through setting up tsup, configuring the output, and using the outExtension
option to customize file extensions.
What is tsup?
tsup is a modern, fast, and zero-configuration bundler for TypeScript and JavaScript projects. It simplifies the process of bundling libraries or applications written in TypeScript or JavaScript, making it easier to produce optimized and production-ready code. tsup uses esbuild under the hood for rapid build times.
What is tsup used for?
tsup is primarily used to bundle TypeScript and JavaScript projects into distributable formats. It automatically handles TypeScript compilation, tree shaking, and bundling without requiring complex configuration. It supports multiple output formats like ESM, CJS, and IIFE, making it versatile for various environments.
It’s ideal for building libraries, applications, or any project that needs to be packaged for deployment. tsup optimizes code by removing unused sections (tree shaking) and minifying output for production. It has native TypeScript support, allowing you to bundle your code directly without precompilation. Additionally, tsup can generate development builds with source maps for debugging and production-ready builds with minification.
tsup minimizes setup complexity by offering a zero-configuration approach, allowing developers to bundle TypeScript and JavaScript projects without extensive configuration files. Unlike traditional bundlers that require complex setups with multiple plugins and custom build scripts, tsup works out of the box by automatically detecting entry points, handling TypeScript compilation, and optimizing output formats. This streamlined workflow significantly reduces the time spent configuring a build system, making it easier to focus on development rather than setup.

If you’re considering alternative bundlers, check out our article “Using Rollup to package a library for TypeScript and JavaScript” for a detailed comparison.
Setting up tsup in a TypeScript project
Before bundling with tsup, start by creating a new TypeScript package. Initialize a project directory and set up TypeScript:
mkdir my-ts-package && cd my-ts-package
npm init -y
npm install typescript --save-dev
npx tsc --init
This initializes a TypeScript package with a default tsconfig.json
.
To integrate tsup
into a TypeScript project, install it via npm:
npm install tsup --save-dev
Then, update the package.json
file to add a build script:
{
"scripts": {
"build": "tsup"
}
}
By default, tsup looks for an index.ts
or src/
index.ts
entry point. To specify an entry file manually, pass it as an argument. For example, if you have a main.ts
file inside src/
, you can define a simple function:
export function greet() {
return "Hello from tsup!";
}
Run the following tsup
command:
npx tsup src/main.ts --format esm,cjs --dts
This command instructs tsup to generate both ESM and CJS outputs and includes TypeScript declaration files (.d.ts
). These files are essential for TypeScript libraries because they provide type definitions that enable editors and compilers to understand the package’s API without needing access to the original TypeScript source. Manually generating these files with tsc
can be cumbersome, requiring additional configurations, but tsup simplifies this by handling it automatically with the --dts
flag.
For those exploring other modern bundling options, our post “Migrating a TypeScript app from Node.js to Bun” offers valuable insights on an emerging alternative.
Customizing output extensions with outExtension
By default, tsup outputs .js
files for both ESM and CJS formats. However, certain environments and packaging requirements may require different extensions. The outExtension
option allows renaming output files.
In a tsup.config.ts
file, define:
import { defineConfig } from 'tsup';
export default defineConfig({
entry: ['src/index.ts'],
format: ['esm', 'cjs'],
dts: true,
outExtension({ format }) {
return format === 'esm' ? { js: '.mjs' } : { js: '.cjs' };
},
});
This configuration ensures that ESM outputs use .mjs
, while CJS outputs use .cjs
, making module resolution more explicit in Node.js environments.
Supporting both ESM and CJS formats
Supporting both ESM and CJS in a TypeScript package is crucial for module format compatibility across different environments. ESM (ECMAScript Modules) is the modern standard, optimized for tree shaking and better performance in bundlers like Webpack and Vite. CJS, on the other hand, is still widely used in Node.js projects and older toolchains. By generating both formats, the package is flexible, allowing users to consume it regardless of their module system.
tsup simplifies this dual support by allowing both formats to be defined in a single command:
npx tsup src/index.ts --format esm,cjs --dts
This approach ensures that both modern and legacy projects can import the package without issues.
For more on the differences between ESM and CommonJS — and why these distinctions matter — see our guide on CommonJS vs. ES modules in Node.js.
How is tsup used in real-world scenarios?
tsup
plays a crucial role in efficiently bundling TypeScript code. The configuration in Mappersmith’s (tulios/mappersmith
) tsup.config.ts
provides an excellent example of setting up bundling for different environments, target versions, and output formats. It showcases how to define entry points, handle different build scenarios like Node.js and browser environments, and manage sourcemaps, type declarations, and minification.
The package.json
script in Mappersmith integrates tsup
as part of a larger build process. It begins by copying version files, running tsup
to bundle the code, and finally generating type declarations. This modular approach keeps the workflow clean and focused on different aspects of the build process. The build script ties together multiple tasks, demonstrating how tsup
fits into a broader toolchain.
The tsup.config.ts
file
For Mappersmith’s tsup
configuration, the following setup is used:
import { defineConfig, Options } from 'tsup'
import { esbuildPluginFilePathExtensions } from 'esbuild-plugin-file-path-extensions'
// Inspired by https://github.com/immerjs/immer/pull/1032/files
export default defineConfig((options) => {
const commonOptions: Partial = {
entry: ['src/**/*.[jt]s', '!./src/**/*.d.ts', '!./src/**/*.spec.[jt]s'],
platform: 'node',
target: 'node16',
// `splitting` should be false, it ensures we are not getting any `chunk-*` files in the output.
splitting: false,
// `bundle` should be false, it ensures we are not getting the entire bundle in EVERY file of the output.
bundle: false,
// `sourcemap` should be true, we want to be able to point users back to the original source.
sourcemap: true,
clean: true,
...options,
}
const productionOptions = {
minify: true,
define: {
'process.env.NODE_ENV': JSON.stringify('production'),
},
}
return [
// ESM, standard bundler dev, embedded `process` references.
// (this is consumed by ["exports" > "." > "import"] and ["exports > "." > "types"] in package.json)
{
...commonOptions,
format: ['esm'],
clean: true,
outDir: './dist/esm/',
esbuildPlugins: [esbuildPluginFilePathExtensions({ filter: /^./ })],
// Yes, bundle: true => https://github.com/favware/esbuild-plugin-file-path-extensions?tab=readme-ov-file#usage
bundle: true,
dts: {
compilerOptions: {
resolveJsonModule: false,
outDir: './dist',
},
},
},
// ESM for use in browsers. Minified, with `process` compiled away
{
...commonOptions,
...productionOptions,
// `splitting` should be true (revert to the default)
splitting: true,
// `bundle` should be true, so we get everything in one file.
bundle: true,
entry: {
'mappersmith.production.min': 'src/index.ts',
},
platform: 'browser',
format: ['esm'],
outDir: './dist/browser/',
},
// CJS
{
...commonOptions,
clean: true,
format: ['cjs'],
outDir: './dist/',
},
]
})
Breakdown of the configuration
In the above setup:
commonOptions
contains settings that apply to all builds, such as defining entry files, targeting Node.js version 16, disabling code splitting, and enabling sourcemaps.productionOptions
applies specifically to production builds, enabling minification and defining theNODE_ENV
variable asproduction
.- The first configuration generates an ESM bundle for Node.js, incorporating the
esbuildPluginFilePathExtensions
plugin and handling TypeScript declarations. - The second configuration bundles ESM code for browsers, applying production optimizations like minification.
- The third configuration generates a CommonJS (CJS) bundle for environments requiring this format.
Handling minified outputs with outExtension
When generating production builds, it is often useful to append .min.js
to minified files for better clarity and organization. The outExtension
option in tsup allows you to modify output file extensions dynamically. Update your configuration as follows:
import { defineConfig } from 'tsup';
export default defineConfig((options) => ({
entry: ['src/index.ts'],
format: ['esm', 'cjs'],
dts: true,
minify: true,
outExtension({ format }) {
return format === 'esm' ? { js: '.min.mjs' } : { js: '.min.cjs' };
},
}));
This setup ensures:
- Minified ESM files are named
*.min.mjs
- Minified CJS files are named
*.min.cjs
This improves clarity when distributing both development and production builds. Explicitly defining file extensions prevents ambiguity in module resolution, particularly in environments requiring strict format handling.
Generating multiple entry points
tsup supports multiple entry points, making it ideal for bundling libraries with several exports. To configure multiple entry points, update your tsup.config.ts
as follows:
import { defineConfig } from 'tsup';
export default defineConfig({
entry: {
index: 'src/index.ts',
utils: 'src/utils.ts',
},
format: ['esm', 'cjs'],
dts: true,
splitting: true,
sourcemap: true,
clean: true,
});
This configuration compiles src/
index.ts
and src/
utils.ts
separately, enabling better modularity and maintainability in larger projects.
Handling assets and external dependencies
If your project includes static assets (such as CSS or JSON files), tsup allows you to exclude external dependencies to keep the final bundle lightweight. Use the external
option to specify dependencies that should not be bundled:
import { defineConfig } from 'tsup';
export default defineConfig({
entry: ['src/index.ts'],
format: ['esm', 'cjs'],
dts: true,
external: ['react', 'lodash'],
});
This ensures that dependencies like react
and lodash
are referenced externally rather than bundled within the output, reducing the final file size and improving efficiency.
By leveraging these configurations, tsup provides a streamlined approach to managing multiple entry points, minified outputs, and external dependencies, making it a powerful tool for modern TypeScript project bundling.
Best practices to avoid common pitfalls
When using tsup
to bundle your TypeScript package, following best practices ensures a smooth and efficient workflow while avoiding common pitfalls. Below are key strategies to use tsup
effectively:
Specify both ESM and CJS formats for maximum compatibility
To ensure your package works across different environments, always specify both ESM (ECMAScript Modules) and CJS (CommonJS) formats. Modern frameworks and tools often prefer ESM, while older systems or Node.js environments may still rely on CJS. Set the format
option in your tsup
configuration:
{
"format": ["esm", "cjs"]
}
Failing to support both formats can limit your package’s usability, so this step is essential.
Generate TypeScript declaration files (.d.ts
)
TypeScript declaration files provide type information for users of your package. Without them, users lose type safety and IntelliSense support. Enable dts
in your tsup
configuration to generate these files:
{
"dts": true
}
Skipping this step can hinder the developer experience for TypeScript users.
Explicitly define outExtension
for Node.js compatibility
Node.js has strict rules for resolving module files, requiring .mjs
for ESM and .cjs
for CJS. To avoid runtime errors, define the outExtension
option in your tsup
configuration:
{
"outExtension": ({ format }) => ({
".js": format === "cjs" ? ".cjs" : ".mjs"
})
}
This ensures Node.js correctly resolves your module files, preventing import issues.
Avoid hardcoding paths in configurations
Hardcoding paths in your tsup
configuration reduces flexibility and makes setup less reusable. Instead, use dynamic options to adapt to different formats or environments. Set the outDir
dynamically:
{
"outDir": ({ format }) => `dist/${format}`
}
This approach keeps your configuration flexible and easier to maintain.
Enable source maps when minifying
Minification reduces bundle size but makes debugging difficult. Enable source maps when minifying to simplify debugging:
{
"minify": true,
"sourcemap": true
}
Without source maps, debugging minified code can be nearly impossible.
Handling dependency resolution issues
By default, tsup treats certain dependencies as external and does not include them in the bundle. If you find missing dependencies in the final output, configure the external
option:
{
"external": ["react", "lodash"]
}
This ensures dependencies like react
and lodash
are referenced externally rather than bundled, reducing file size.
Performance optimizations
While tsup
is optimized for speed, enabling certain features like source maps can impact build times. Monitor your build performance and adjust configurations as needed to balance speed and debugging capabilities.
Conclusion
tsup
is a powerful bundler that simplifies the process of bundling TypeScript projects. Its support for ESM and CJS formats, along with features like outExtension
for customized file extensions, makes it an essential tool for modern JavaScript development. By following these best practices, you can effectively integrate tsup
into your workflow, ensuring efficient and production-ready builds. Whether you are focusing on tree shaking, module format compatibility, or streamlined builds, tsup
provides the necessary tools for success.
