Skip to content

package.json exports + Types: Designing a Clean Public API

How to structure multi-entry TypeScript packages with package.json exports, prevent deep imports, and ship correct .d.ts for consumers.

Rajesh DhimanMarch 01, 20262 min read
On this page

What you’ll learn

  • Why exports is the real public API (not your folder structure)
  • How to ship correct .d.ts for each entry point
  • How to prevent “deep import” dependency traps (pkg/dist/internal)
  • A production-ready package layout you can copy

30‑second interview answer

For modern Node/bundlers, package.json exports defines what paths consumers can import. If you don’t configure it, consumers can deep-import internal files, locking you into accidental APIs. A high-quality TS package ships matching JS + .d.ts for each export entry and keeps the surface small and stable.


Mental model: “Your folders are not your API”

Consumers don’t import “your codebase”; they import paths:

import { foo } from "@acme/utils";
import { slugify } from "@acme/utils/string";

If you don’t control those paths, users will do this:

// Don’t let this become possible:
import { secretInternalFn } from "@acme/utils/dist/internal/secret";

Then you can never change internals without breaking people.


package/
  src/
    index.ts
    string.ts
  dist/
    index.js
    index.d.ts
    string.js
    string.d.ts
  package.json
  tsconfig.build.json

The exports map (copy/paste starting point)

{
  "name": "@acme/utils",
  "version": "1.0.0",
  "type": "module",
  "main": "./dist/index.js",
  "types": "./dist/index.d.ts",
  "exports": {
    ".": {
      "types": "./dist/index.d.ts",
      "default": "./dist/index.js"
    },
    "./string": {
      "types": "./dist/string.d.ts",
      "default": "./dist/string.js"
    }
  }
}

Why both types and exports?

  • types at the top is a fallback for older tooling.
  • Per-entry exports["./x"].types ensures each subpath has correct typings.

Example code (what consumers should see)

// src/string.ts
export function slugify(input: string) {
  return input
    .trim()
    .toLowerCase()
    .replace(/\s+/g, "-")
    .replace(/[^a-z0-9-]/g, "");
}
// src/index.ts
export { slugify } from "./string";

Now consumers can do:

import { slugify } from "@acme/utils";
// or
import { slugify } from "@acme/utils/string";

The 3 common failure modes

1) JS exists but .d.ts doesn’t

Symptom: runtime works, TypeScript errors.

Fix: ensure build emits declarations:

  • declaration: true
  • and your build pipeline copies .d.ts to dist/

2) .d.ts exists but points to the wrong module kind

Symptom: types resolve but tooling complains about ESM/CJS mismatch.

Fix: align TS build module/moduleResolution with runtime target (NodeNext for ESM packages).

3) Deep imports leak your internals

Symptom: consumers import dist/* paths.

Fix: add an exports map and do not export internals.


Production rule of thumb

  • Every public import path should be explicitly listed under exports.
  • Ship only stable entry points.
  • If you wouldn’t document it, don’t export it.

Interview questions

  1. Q: What problem does exports solve?

    • A: It defines the allowed import paths and prevents accidental public APIs via deep imports.
  2. Q: How do you ship typings for multiple entry points?

    • A: Emit .d.ts per entry and map them in exports under types.
  3. Q: Why is this important for long-term maintenance?

    • A: It lets you refactor internals without breaking consumers.

Quick recap

  • exports is your real API surface.
  • Map JS + .d.ts for every entry.
  • Prevent deep imports to keep your package maintainable.
Companion Book

Looking for structured, offline interview prep?

Free online lessons • Portable Amazon Kindle / Paperback

Frontend Interview Guide (2025 Edition) cover
Recommended for React & Frontend Rounds

Frontend Interview Guide (2025 Edition)

By Rajesh Dhiman

Comprehensive preparation covering React internals, Web Performance, CSS architecture, and system design questions.

View on Amazon ↗Kindle & Paperback
JavaScript Interview Guide cover
Recommended for Core JS Rounds

JavaScript Interview Guide

By Rajesh Dhiman

Deep dive into the Event Loop, microtask draining, closures, prototypal inheritance, and asynchronous execution patterns.

View on Amazon ↗Kindle & Paperback

Note: All lessons on Frontend Interview remain completely free to read online. As an Amazon Associate, we may earn from qualifying purchases.

RD

Written by Rajesh Dhiman

Authored by Rajesh Dhiman and engineered at Eunix Tech. Focused on deep-dive frontend engineering, JavaScript runtime internals, React architecture, and web performance.