Arrange Act Assert

Jag Reehals thinking on things, mostly product development

Building TypeScript Libraries' A Guide to Explicit Exports, Tree Shaking, and Common Pitfalls

20 Feb 2025

builder building typescript sign

Building a TypeScript library that is both maintainable and optimised for modern bundlers requires careful consideration of exporting functions, types, and other constructs from your modules.

With several strategies available, from wildcard re-exports to namespaced exports, explicitly named re-exports have emerged as the clear winner. This post explores the alternatives with examples based on an order management system.

We'll discuss the benefits of exporting types, how to organise multiple entry routes (such as in /src/orders and /src/users), and review the necessary TypeScript configuration options (such as verbatimModuleSyntax) and package.json tweaks. Additionally, we'll explain how this approach helps prevent circular dependency challenges by enforcing a clear dependency graph.

Building a library

Imagine you are developing a library for an order management system that handles order lookups, cancellations, and creation. Your project is organised into distinct domains:

Your public API should enable consumers to import functions and types naturally. For instance, they should be able to write:

import { lookupOrder, cancelOrder } from './orders';
import type { Order } from './orders';
import { getUser, updateUser } from './users';

The challenge is organising your exports to achieve maximum tree-shaking efficiency, avoid naming collisions, and offer a seamless experience for value and type-only imports.

Alternative Exporting Approaches

1. Re-Exporting with export * from

A common method is to re-export everything from a module:

// src/orders/index.ts
export * from './lookup';
export * from './commands';

Pros:

Cons:

2. Namespace Exports with export * as ns from

Another option is to group exports under a namespace:

// src/orders/index.ts
export * as lookup from './lookup';
export * as commands from './commands';

Consumers would then import and access functions as follows:

import { lookup } from './orders';
lookup.lookupOrder(...);

Pros:

Cons:

3. Explicit Named Re-Exports (Our Recommended Approach)

We recommend explicitly listing the exports in your domain's index file. For example, in the domain of the order:

// src/orders/index.ts
export { lookupOrder, cancelOrder } from './lookup';
export { createOrder } from './commands';
export type { Order, OrderStatus } from './types';

And in the users domain:

// src/users/index.ts
export { getUser, listUsers } from './queries';
export { updateUser } from './commands';

Pros:

Cons:

TypeScript Configuration & Package.json Adjustments

To fully leverage explicit named re-exports in the ESM era, update your tsconfig.json as follows:

{
  "compilerOptions": {
    "module": "esnext",
    "target": "esnext",
    "verbatimModuleSyntax": true,
    // other options...
  }
}

The verbatimModuleSyntax option ensures that TypeScript leaves your ES module import/export statements untouched, essential for effective tree shaking by modern bundlers.

In your package.json, expose your public API entry points:

{
  "name": "my-library",
  "version": "1.0.0",
  "type": "module",
  "exports": {
    "./orders": "./dist/orders/index.js",
    "./users": "./dist/users/index.js"
  },
  "scripts": {
    "build": "tsc"
  }
}

Multiple Entry Routes: A Concrete Example

Assuming your project is structured like this:

src/
  orders/
    lookup.ts         // Contains lookupOrder, cancelOrder, etc.
    commands.ts       // Contains createOrder.
    types.ts          // Contains Order, OrderStatus, etc.
    index.ts          // Explicitly re-exports functions and types.
  users/
    queries.ts        // Contains getUser, listUsers, etc.
    commands.ts       // Contains updateUser.
    index.ts

Exporting Types: Why It Matters

Explicitly exporting types along with functions offers several advantages:

For example, rather than using a wildcard re-export for types:

// Avoid: Not ideal for public libraries
export * from './types';

It is preferable to use explicit type exports:

export type { Order, OrderStatus } from './types';

Conclusion

Explicitly named re-exports remain the most recommended approach for public libraries in 2025. They provide:

Organising your exports explicitly in each domain's index file for a library with multiple domains, such as orders and users, ensures that your public API is robust, optimised, and easy to maintain as the ecosystem evolves.

Hopefully, this guide helps you build more maintainable, optimised, and future-proof libraries.

typescript