To Bundle or Not to Bundle Your TypeScript Types
Even for experienced TypeScript engineers, the difference between “bundled” and “unbundled” types can be confusing. Digging into this topic can help you create better libraries and consume 3rd-party libraries better.
What is Bundling?
When building a TypeScript library with tools like tsc
, the module structure is preserved by default.
For example, if your library has a src/index.ts
and src/utils.ts
file, you will see dist/index.d.ts
and dist/utils.d.ts
in the build output. In this case, your types are unbundled.
If you explore types of an unbundled library, e.g., by cmd + click
in VS Code, your IDE will take you
to the declaration file where that type was defined. The declaration file will only contain the types of that module
and not the types of the whole library.
In contrast, bundling d.ts
files means combining d.ts
files into a single file that contains all the types for your entire library.
Bundling is typically done with tools like rollup-plugin-dts
. With bundling, the module
structure of your source files is lost.
If you explore the types of a bundled library, you navigate through one large file. Let’s explore when it makes sense to bundle your TypeScript types.
TL;DR
The general best practice is: type declarations should align with your JavaScript distribution files. So, if you use a bundler to bundle all your TypeScript files into one large bundled JavaScript file, you should bundle your types into one large file. This ensures that:
- Your JavaScript and
d.ts
files are consistent. - Your declaration files don’t expose files that don’t exist in your JavaScript bundle (this happens when your JavaScript is bundled but your declarations are not).
To answer whether you should bundle your declaration files is to answer the question of whether you should bundle your JavaScript files. Use this rule of thumb: Don’t bundle your types until you have a good reason to bundle them.
Good Reasons to Bundle JavaScript Files
- Your library is consumed by an environment that expects a bundled file (e.g., browser or CommonJS).
- Consumers of your library do not use a bundler and require a small library footprint.
When to Keep Types Unbundled
Not bundling your types is a good default because:
- Most consumer code uses a bundler before shipping to the browser or running on the server.
- You can use declaration maps to make navigating types easier by linking to source code in editors.
A Special Case for the exports
Property?
The exports
property of the package.json
file
allows defining entry points of a package. It’s supported in Node.js 12+ and all other modern JavaScript runtimes.
{
"exports": {
".": "./dist/index.js",
"./utils": "./dist/utils.js"
},
"types": "./dist/index.d.ts"
}
One feature of the exports
property is restricting module imports. It prevents consumers of a library from importing internal files.
When the exports
property is specified, imports like import { X } from 'lib/internal/foo'
will fail unless lib/internal/foo
is a
valid entry point listed in the exports
property.
In many ways, defining a root entry point with the exports
property for .
makes your library behave like it is bundled - even if it is not.
This is because the exports
property hides internal APIs the same way bundling does.
When using the exports
property, you might think there’s a stronger to use bundled types, since you’re emulating the behavior
of a bundled library. However, even when using the exports
property, having **unbundled types should still be your default since it
adds less complexity to your build setup. For multi-entry libraries, TypeScript can resolve unbundled types (e.g., dist/utils.d.ts for lib/utils)
as long as they match your exports paths.
If you bundle types and have multiple entry points, consider bundling per entry point with tools like rollup-plugin-dts to maintain modularity.