| |

TypeScript 61 🔷 Publishing Typed Packages

Publishing a TypeScript package means shipping two things: the JavaScript that runs and the declarations that describe it. The first is what the consumer’s runtime executes; the second is what the consumer’s compiler reads. Getting both right is the difference between a library that is usable in every project and a library that produces “Cannot find module” errors for half its consumers. The .d.ts files must be generated, the package.json must point to them correctly, the exports map must resolve the types under every module resolution mode, and the publish itself must be secure. This chapter covers the full path: the build configuration that emits declarations, the package.json fields that publish them, the exports map that resolves them, the tools that verify them, and the publishing workflow that keeps the package trustworthy.

Key point: A typed package ships .d.ts files alongside the .js files, and the package.json points to them with the types field and the types condition in the exports map. The types condition must be the first entry in every exports block, because the resolver walks top-down and stops at the first match. The declaration: true compiler option generates the .d.ts files, and the declarationMap: true option generates the source maps that let the consumer’s editor jump to the original source. The @arethetypeswrong/cli tool simulates every consumer resolution mode and reports the broken paths. The publish itself uses trusted publishing with OIDC and provenance, which removes the long-lived token and produces a signed attestation.


Why publishing types is different from publishing code

A JavaScript library ships one artifact: the .js file. A TypeScript library ships two: the .js file that runs and the .d.ts file that describes it. The two are separate, and the consumer’s toolchain uses them for different purposes.

The runtime uses the .js. Node.js, the browser, and the bundler execute the .js file. The .d.ts file is not executed and is not present at runtime.

The compiler uses the .d.ts. The consumer’s TypeScript compiler reads the .d.ts file to check the types. If the file is missing, the import is typed as any, and the consumer loses the type safety.

Why the two must agree. The .d.ts describes what the .js does. If the description is wrong — a function takes different arguments, a property does not exist — the consumer’s code compiles but fails at runtime. The .d.ts must be generated from the source, not written by hand, so the two cannot drift.

Why the generated .d.ts is preferred. The declaration: true option makes the compiler emit the .d.ts files from the source. The generated files are always in sync with the .js, and the publish is a matter of pointing to them .

Why the hand-written .d.ts is the exception. A library written in plain JavaScript has no types to generate. The types are written by hand or published as an @types package. The hand-written file must be maintained separately, and the drift is the risk .

Why the types are part of the public interface. The .d.ts files are the library’s contract. The consumer’s code is checked against them, and a change to the types is a change to the API. The semantic versioning applies to the types as much as to the runtime .


Generating the declarations

The compiler generates the .d.ts files when the declaration option is set. The files go to the same output directory as the .js files.

// tsconfig.json
{
  "compilerOptions": {
    "target": "es2022",
    "module": "nodenext",
    "moduleResolution": "nodenext",
    "declaration": true,
    "declarationMap": true,
    "outDir": "./dist",
    "strict": true
  }
}

The declaration: true option emits the .d.ts files. The declarationMap: true option emits the .d.ts.map files, which link the declarations to the original source.

Why the declarationMap matters. The map lets the consumer’s editor jump to the original TypeScript source when they use “Go to Definition” on an imported symbol. Without the map, the editor shows the generated .d.ts, which is less useful.

Why the outDir must be set. The declarations are emitted to the outDir, which is the same directory as the .js files. The dist directory holds both, and the package.json points to them.

Why the emitDeclarationOnly option exists. A library that is bundled by a separate tool can set emitDeclarationOnly: true to emit only the .d.ts files, and let the bundler produce the .js. The option is for the build pipelines where the JavaScript is produced by a bundler and the declarations by the compiler .

Why the stripInternal option matters. The stripInternal: true option removes the declarations marked with the @internal JSDoc tag from the emitted .d.ts. The option keeps the internal members out of the public interface, and the consumer does not see them.

/** @internal */
export function internalHelper(): void {}

The function is marked as internal, and the stripInternal option removes it from the .d.ts.

Why the build must be verified. The npm run build command produces the dist directory, and the ls dist command shows the .js and .d.ts files. The verification is the first step, and the missing declarations are caught before the publish.


The package.json fields

The package.json declares the entry points and the types. The fields have evolved from the legacy main and types to the modern exports map.

The legacy types field. The types field points to the .d.ts entry point. It is the classic mechanism, and it is read by the node module resolution.

{
  "name": "my-library",
  "main": "./dist/index.js",
  "types": "./dist/index.d.ts"
}

The main is the runtime entry point, and the types is the declaration entry point. The two are the pair, and the types is the one the compiler reads .

Why the types field is not enough. The types field is read by the node resolution, which is the legacy mode. The modern modes — node16, nodenext, bundler — read the exports map instead. A package with only the types field resolves correctly in the legacy mode and fails in the modern modes .

The modern exports map. The exports field declares the entry points and the conditions. The types condition provides the declarations for each entry.

{
  "name": "my-library",
  "type": "module",
  "main": "./dist/index.cjs",
  "module": "./dist/index.js",
  "types": "./dist/index.d.ts",
  "exports": {
    ".": {
      "types": "./dist/index.d.ts",
      "import": "./dist/index.js",
      "require": "./dist/index.cjs"
    },
    "./utils": {
      "types": "./dist/utils.d.ts",
      "import": "./dist/utils.js"
    },
    "./package.json": "./package.json"
  }
}

The exports["."] is the main entry point. The exports["./utils"] is a subpath. The types condition is the first in each block, and the import and require are the runtime conditions.

Why the types condition must be first. The conditions are checked in order, and the first match wins. If import comes before types, the compiler resolves to the .js file and does not find the declarations. The types must be first .

Why the require condition is needed. A package that has a CommonJS consumer needs the require condition, which points to the .cjs file and its declarations. Without it, the CJS consumer cannot import the package .

Why the ./package.json export is needed. The tools — Vite, Webpack, the monorepo linkers — read the package.json. The "./package.json": "./package.json" entry exposes it, and the tools that need it can resolve it .

Why the files allowlist matters. The files field lists what is published. The dist directory holds the build output, and the allowlist includes it. The src and the tests are not published .

{
  "files": ["dist", "README.md", "LICENSE"]
}

Why the sideEffects field matters. The sideEffects: false field tells the bundler that the package has no side effects, which enables the tree-shaking. The field is set when the modules are pure, and the bundler can remove the unused imports.


The dual ESM/CJS build

A modern package often ships both the ESM and the CJS builds, so that the consumers on either module system can import it. The build produces the two sets of files, and the exports map selects the right one.

The two builds. The ESM build is the .js files with the import and export syntax. The CJS build is the .cjs files with the require and module.exports syntax.

dist/
├── index.js        ← ESM
├── index.cjs       ← CJS
├── index.d.ts      ← ESM declarations
├── index.d.cts     ← CJS declarations
├── utils.js
├── utils.cjs
├── utils.d.ts
└── utils.d.cts

Why the .d.cts files. A package with "type": "module" treats the .js files as ESM. The CJS files use the .cjs extension, and the declarations for the CJS files use the .d.cts extension. The extension tells the compiler which module system the declarations describe .

Why the declarations must match the module system. The declarations for an ESM module use the import and export syntax. The declarations for a CJS module use the export = syntax or the export default. The two are different, and the wrong one produces the resolution errors .

Why the dual build is the complexity. The dual build doubles the output and the declarations, and the two must agree. The arethetypeswrong tool checks the agreement, and the common mistakes are the “masquerading” errors — the ESM types served for a CJS entry, or the reverse .

Why the dual build is not always needed. A library that targets only the modern consumers can ship only the ESM. A library that targets only the legacy consumers can ship only the CJS. The dual build is for the libraries that must support both, and the cost is the complexity .

Why the bundlers handle the dual build. The bundlers — tsup, unbuild, rollup — produce the two builds and the declarations from a single source. The configuration is a few lines, and the tooling handles the extension mapping .

tsup src/index.ts --format esm,cjs --dts

The tsup command produces the ESM and CJS builds and the declarations. The --dts flag is the declaration generation, and the tooling maps the extensions .


The typesVersions fallback

The typesVersions field is the legacy mechanism for the type resolution under the old TypeScript versions and the node module resolution. It is the fallback for the consumers that do not read the exports map.

{
  "typesVersions": {
    "*": {
      "*": ["dist/*"],
      "utils": ["dist/utils.d.ts"]
    }
  }
}

The typesVersions maps the import paths to the declaration paths for the consumers that use the node resolution. The * is the wildcard, and the mapping is the fallback.

Why the typesVersions is still needed. The Jest test environment and the older toolchains use the node resolution, which does not read the exports map. The typesVersions is the fallback that makes the subpath imports resolve in those environments .

Why the typesVersions overlaps with the exports. The exports map’s types condition is the modern mechanism, and the typesVersions is the legacy one. The two are maintained together during the migration, and the typesVersions is dropped when the node resolution is no longer a concern .

Why the typesVersions is a maintenance cost. The map must be kept in sync with the exports map, and the two can drift. The modern tooling prefers the exports, and the typesVersions is the compatibility layer.

Why the typesVersions uses the wildcard. The "*": ["dist/*"] mapping resolves the import my-library/utils to dist/utils.d.ts. The wildcard is the pattern, and the mapping is the substitution.

Why the typesVersions should be tested. The arethetypeswrong tool checks the resolution under the node10 mode, and the typesVersions is the mechanism that makes it pass. The test is the verification .


Verifying the package

The arethetypeswrong tool simulates every consumer resolution mode and reports the broken paths. It is the standard verification before the publish.

The attw command.

npx @arethetypeswrong/cli --pack .

The command packs the package and checks the resolution under the node10, node16, and bundler modes. The output lists the problems and the affected paths .

The common problems. The tool reports the “masquerading” errors — the types that claim to be ESM but describe a CJS module, or the reverse. The errors are the common mistakes in the dual build, and the tool catches them .

Why the node10 check matters. The node10 mode is the legacy resolution, and it does not read the exports map. The typesVersions is the fallback, and the check verifies that the fallback works. A package that passes the node16 and bundler checks but fails the node10 check has the legacy consumers broken .

The publint tool. The publint checks the package.json configuration against the actual output files. It verifies that the exports, main, module, and types fields point to the files that exist .

npx publint

Why the two tools are complementary. The attw checks the type resolution, and the publint checks the configuration. The two together cover the package’s publishability, and the CI runs both .

Why the tools should be in the CI. The verification is before the publish, and the CI runs it on every commit. The broken resolution is caught before the release, and the fix is the configuration change.


The publish workflow

The publish is the final step, and the security matters. The modern workflow uses the trusted publishing with the OIDC, which removes the long-lived token and produces the provenance.

The trusted publishing. The npm’s trusted publishing uses the OIDC to authenticate the CI workflow. The workflow is configured as the trusted publisher for the package, and the publish is authenticated without a stored token .

# .github/workflows/publish.yml
permissions:
  id-token: write
  contents: read

jobs:
  publish:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: 22
          registry-url: https://registry.npmjs.org
      - run: npm ci
      - run: npm run build
      - run: npm publish --provenance --access public

The id-token: write permission is required for the OIDC. The npm publish --provenance produces the signed attestation, and the --access public is required for the scoped packages .

Why the provenance matters. The provenance is the signed attestation that links the published tarball to the exact source commit and the workflow that built it. The consumers can verify the package was not tampered with, and the badge appears on the npm page .

Why the trusted publishing removes the token. The long-lived NPM_TOKEN is the security risk. The trusted publishing uses the short-lived OIDC credentials, and there is no token to steal. The secret is removed from the CI, and the publish is authenticated by the workflow’s identity .

Why the prepublishOnly script matters. The prepublishOnly runs before the publish, and it is the place for the tests and the build. The script ensures the published artifact is the built one, not the stale one .

{
  "scripts": {
    "build": "tsup src/index.ts --format esm,cjs --dts",
    "test": "vitest run",
    "prepublishOnly": "npm run test && npm run build"
  }
}

Why the npm pack --dry-run matters. The npm pack --dry-run lists the files that would be published. The review of the list catches the accidental inclusions — the .env, the tests, the source — before the publish .

Why the 2FA matters. The two-factor authentication on the npm account and on the package’s publish setting is the protection against the account takeover. The maintainer’s account is the attack vector, and the 2FA is the defense .


Complete Example Session

// ============================================
// PART 1: THE SOURCE
// ============================================

// src/index.ts
export interface User {
  id: string;
  name: string;
}

export function getUser(id: string): Promise<User> {
  return fetch(`/api/users/${id}`).then((r) => r.json());
}

export class UserService {
  constructor(private readonly baseUrl: string) {}
  getUser(id: string): Promise<User> {
    return fetch(`${this.baseUrl}/users/${id}`).then((r) => r.json());
  }
}

// ============================================
// PART 2: THE TSCONFIG
// ============================================

// tsconfig.json
// {
//   "compilerOptions": {
//     "target": "es2022",
//     "module": "nodenext",
//     "moduleResolution": "nodenext",
//     "declaration": true,
//     "declarationMap": true,
//     "outDir": "./dist",
//     "strict": true
//   }
// }

// ============================================
// PART 3: THE EMITTED DECLARATIONS
// ============================================

// dist/index.d.ts
// export interface User {
//   id: string;
//   name: string;
// }
//
// export declare function getUser(id: string): Promise<User>;
//
// export declare class UserService {
//   private readonly baseUrl;
//   constructor(baseUrl: string);
//   getUser(id: string): Promise<User>;
// }

// ============================================
// PART 4: THE PACKAGE.JSON
// ============================================

// package.json
// {
//   "name": "my-library",
//   "version": "1.0.0",
//   "type": "module",
//   "main": "./dist/index.cjs",
//   "module": "./dist/index.js",
//   "types": "./dist/index.d.ts",
//   "exports": {
//     ".": {
//       "types": "./dist/index.d.ts",
//       "import": "./dist/index.js",
//       "require": "./dist/index.cjs"
//     },
//     "./package.json": "./package.json"
//   },
//   "files": ["dist", "README.md", "LICENSE"],
//   "sideEffects": false,
//   "scripts": {
//     "build": "tsup src/index.ts --format esm,cjs --dts",
//     "test": "vitest run",
//     "prepublishOnly": "npm run test && npm run build"
//   }
// }

// ============================================
// PART 5: THE DUAL BUILD
// ============================================

// dist/
// ├── index.js        ← ESM
// ├── index.cjs       ← CJS
// ├── index.d.ts      ← ESM declarations
// ├── index.d.cts     ← CJS declarations
// └── index.d.ts.map

// ============================================
// PART 6: THE TYPESVERSIONS FALLBACK
// ============================================

// package.json (added)
// {
//   "typesVersions": {
//     "*": {
//       "*": ["dist/*"]
//     }
//   }
// }

// ============================================
// PART 7: THE VERIFICATION
// ============================================

// npx @arethetypeswrong/cli --pack .
// npx publint

// ============================================
// PART 8: THE PUBLISH WORKFLOW
// ============================================

// .github/workflows/publish.yml
// permissions:
//   id-token: write
//   contents: read
//
// jobs:
//   publish:
//     runs-on: ubuntu-latest
//     steps:
//       - uses: actions/checkout@v4
//       - uses: actions/setup-node@v4
//         with:
//           node-version: 22
//           registry-url: https://registry.npmjs.org
//       - run: npm ci
//       - run: npm run build
//       - run: npm publish --provenance --access public

// ============================================
// PART 9: THE DRY RUN
// ============================================

// npm pack --dry-run
// npm notice 📦  my-library@1.0.0
// npm notice Tarball Contents
// npm notice 1.2kB dist/index.js
// npm notice 0.8kB dist/index.cjs
// npm notice 0.5kB dist/index.d.ts
// npm notice 0.4kB dist/index.d.cts
// npm notice 1.1kB README.md
// npm notice 1.1kB LICENSE
// npm notice Tarball Details
// npm notice name: my-library
// npm notice version: 1.0.0
// npm notice package size: 1.5 kB

// ============================================
// PART 10: WHAT NOT TO DO
// ============================================

// Don't put types after import in the exports map
// The resolver stops at the first match.

// Don't forget the .d.cts for the CJS build
// The declarations must match the module system.

// Don't ship the .js without the .d.ts
// The consumers get no types.

// Don't use a long-lived NPM_TOKEN
// Use trusted publishing with OIDC.

// Don't skip the attw verification
// The broken resolution is caught before the publish.

// Don't forget the files allowlist
// The .env and the tests are published by default.

The ten parts cover the source, the tsconfig, the emitted declarations, the package.json, the dual build, the typesVersions, the verification, the workflow, the dry run, and the anti-patterns.


Quick Reference

The tsconfig.json Options

OptionEffect
declarationEmit .d.ts files
declarationMapEmit .d.ts.map files
emitDeclarationOnlyEmit only declarations
stripInternalRemove @internal
outDirThe output directory

The package.json Fields

FieldPurpose
mainLegacy CJS entry
moduleBundler ESM entry
typesLegacy types entry
exportsModern entry map
typesVersionsLegacy subpath types
filesPublish allowlist
sideEffectsTree-shaking hint

The exports Conditions

ConditionPurpose
typesThe declarations (first)
importThe ESM entry
requireThe CJS entry
defaultThe fallback (last)

The Declaration Extensions

ExtensionModule System
.d.tsESM (in a "type": "module" package)
.d.ctsCJS
.d.mtsESM (explicit)

The Verification Tools

ToolPurpose
@arethetypeswrong/cliCheck the type resolution
publintCheck the package.json
npm pack --dry-runPreview the published files

The Publish Security

MeasurePurpose
Trusted publishingOIDC, no long-lived token
--provenanceSigned attestation
2FAAccount protection
files allowlistMinimal published files
prepublishOnlyBuild and test gate

Best Practices

✅ Do This:

// Generate the declarations
{ "compilerOptions": { "declaration": true, "declarationMap": true } } // ✅
// Put types first in every exports block
"exports": { ".": { "types": "./dist/index.d.ts", "import": "./dist/index.js" } } // ✅
// Include the require condition for the CJS consumers
"exports": { ".": { "types": "./dist/index.d.ts", "require": "./dist/index.cjs" } } // ✅
// Expose the package.json
"exports": { "./package.json": "./package.json" }              // ✅
# Verify before the publish
npx @arethetypeswrong/cli --pack .
npx publint
npm pack --dry-run                                             # ✅
# Use trusted publishing
permissions:
  id-token: write
steps:
  - run: npm publish --provenance --access public              # ✅

❌ Don’t Do This:

// Don't put import before types
"exports": { ".": { "import": "./dist/index.js", "types": "./dist/index.d.ts" } } // ⚠️
// Don't ship the .js without the .d.ts
"main": "./dist/index.js"  // no types                           // ⚠️
// Don't forget the .d.cts for the CJS build
"exports": { ".": { "require": "./dist/index.cjs" } }  // no types // ⚠️
# Don't use a long-lived NPM_TOKEN
NPM_TOKEN=...  # use trusted publishing                          # ⚠️
# Don't publish without the dry run
npm publish  # review the files first                             # ⚠️
// Don't forget the files allowlist
{ "files": ["dist"] }  // or the .env is published               // ⚠️

Common Pitfalls

PitfallProblemSolution
types after importThe compiler picks .jsPut types first
Missing .d.ctsThe CJS types are wrongGenerate the dual declarations
No typesVersionsThe node10 resolution failsAdd the fallback
Missing require conditionThe CJS consumers failAdd the require
No ./package.json exportThe tools failExpose it
Long-lived tokenThe security riskUse the trusted publishing
No dry runThe junk is publishednpm pack --dry-run
No verificationThe resolution is brokenattw and publint

Real-World Examples

1. The declaration option

{ "compilerOptions": { "declaration": true } }

2. The types condition

"exports": { ".": { "types": "./dist/index.d.ts" } }

3. The dual build

tsup src/index.ts --format esm,cjs --dts

4. The require condition

"exports": { ".": { "types": "./dist/index.d.ts", "require": "./dist/index.cjs" } }

5. The package.json export

"exports": { "./package.json": "./package.json" }

6. The files allowlist

{ "files": ["dist", "README.md", "LICENSE"] }

7. The typesVersions

{ "typesVersions": { "*": { "*": ["dist/*"] } } }

8. The verification

npx @arethetypeswrong/cli --pack .

9. The dry run

npm pack --dry-run

10. The trusted publish

- run: npm publish --provenance --access public

Visual: The Build Output

┌──────────────────────────────────────────────────────────┐
│  src/index.ts                                            │
│       │                                                  │
│       │  tsc with declaration: true                      │
│       ▼                                                  │
│  dist/                                                   │
│    index.js        ← ESM                                 │
│    index.cjs       ← CJS                                 │
│    index.d.ts      ← ESM declarations                    │
│    index.d.cts     ← CJS declarations                    │
│    index.d.ts.map  ← source map                          │
│                                                          │
│  The .js is for the runtime.                             │
│  The .d.ts is for the consumer's compiler.               │
│                                                          │
└──────────────────────────────────────────────────────────┘

Visual: The exports Map

┌──────────────────────────────────────────────────────────┐
│  "exports": {                                            │
│    ".": {                                                │
│      "types": "./dist/index.d.ts",  ← first              │
│      "import": "./dist/index.js",                        │
│      "require": "./dist/index.cjs"                       │
│    },                                                    │
│    "./utils": {                                          │
│      "types": "./dist/utils.d.ts",                       │
│      "import": "./dist/utils.js"                         │
│    },                                                    │
│    "./package.json": "./package.json"                    │
│  }                                                       │
│                                                          │
│  The types must be the first condition in each block.    │
│  The require is for the CJS consumers.                   │
│  The ./package.json is for the tools.                    │
│                                                          │
└──────────────────────────────────────────────────────────┘

Visual: The Consumer Resolution

┌──────────────────────────────────────────────────────────┐
│  THE CONSUMER'S TSCONFIG                                 │
│                                                          │
│  "moduleResolution": "node16"                            │
│       │                                                  │
│       ▼                                                  │
│  Reads the exports map.                                  │
│  Picks the "types" condition.                            │
│  Resolves the declarations.                              │
│                                                          │
├──────────────────────────────────────────────────────────┤
│  "moduleResolution": "bundler"                           │
│       │                                                  │
│       ▼                                                  │
│  Reads the exports map.                                  │
│  Picks the "types" condition.                            │
│  Resolves the declarations.                              │
│                                                          │
├──────────────────────────────────────────────────────────┤
│  "moduleResolution": "node" (legacy)                     │
│       │                                                  │
│       ▼                                                  │
│  Does NOT read the exports map.                          │
│  Reads the "types" field and the "typesVersions".        │
│  Resolves the declarations.                              │
│                                                          │
│  All three must work. The attw verifies.                 │
│                                                          │
└──────────────────────────────────────────────────────────┘

Visual: The Publish Security

┌──────────────────────────────────────────────────────────┐
│  WITHOUT TRUSTED PUBLISHING                              │
│                                                          │
│  The NPM_TOKEN secret is stored in the CI.               │
│  The token is long-lived.                                │
│  The token can be stolen.                                │
│  No provenance.                                          │
│                                                          │
├──────────────────────────────────────────────────────────┤
│  WITH TRUSTED PUBLISHING                                 │
│                                                          │
│  The OIDC credentials are short-lived.                   │
│  No token is stored.                                     │
│  The provenance is automatic.                            │
│  The tarball is linked to the commit.                    │
│                                                          │
│  The secret is removed.                                  │
│                                                          │
└──────────────────────────────────────────────────────────┘

Visual: The Verification

┌──────────────────────────────────────────────────────────┐
│  npx @arethetypeswrong/cli --pack .                      │
│       │                                                  │
│       ▼                                                  │
│  Checks the resolution under:                            │
│    node10   → the typesVersions                          │
│    node16   → the exports                                │
│    bundler  → the exports                                │
│                                                          │
│  Reports:                                                │
│    ✅ resolves correctly                                 │
│    ❌ masquerading as ESM/CJS                            │
│    ❌ no resolution                                      │
│                                                          │
├──────────────────────────────────────────────────────────┤
│  npx publint                                             │
│       │                                                  │
│       ▼                                                  │
│  Checks the package.json against the output.             │
│  Reports the missing files and the wrong paths.          │
│                                                          │
│  Both must pass before the publish.                      │
│                                                          │
└──────────────────────────────────────────────────────────┘

Summary

ItemValue
Declaration generationdeclaration: true
Declaration mapdeclarationMap: true
Legacy types entrytypes field
Modern types entrytypes condition in exports
Types orderFirst in every block
Dual build.js, .cjs, .d.ts, .d.cts
Legacy fallbacktypesVersions
Verificationattw, publint
Publish securityTrusted publishing, provenance

Key takeaways:

  • A typed package ships the .js files and the .d.ts files — the first is the runtime, the second is the compiler’s contract, and the two must agree
  • The declaration: true option generates the .d.ts files — the generated files are always in sync with the source, and the declarationMap: true adds the source maps
  • The types field is the legacy entry point, and the types condition in exports is the modern one — the modern modes read the exports map, and the legacy mode reads the types field
  • The types condition must be the first in every exports block — the resolver stops at the first match, and the wrong order produces the “Cannot find module” errors
  • The dual ESM/CJS build produces the .js and .cjs files and the .d.ts and .d.cts declarations — the extensions must match the module system, and the arethetypeswrong tool checks the agreement
  • The typesVersions field is the legacy fallback — it makes the subpath imports resolve under the node10 mode, and it is maintained alongside the exports during the migration
  • The ./package.json export is needed for the tooling — the bundlers and the monorepo linkers read the package.json, and the explicit entry exposes it
  • The files allowlist controls what is published — the dist directory holds the build output, and the src, the tests, and the secrets are excluded
  • The arethetypeswrong and publint tools verify the package before the publish — the first checks the type resolution, the second checks the configuration, and both must pass
  • The trusted publishing with OIDC and provenance is the modern security — the long-lived token is removed, the short-lived credentials authenticate the workflow, and the signed attestation links the tarball to the source

Remember: Publishing a typed package means shipping the JavaScript and the declarations, and making sure the two agree. The declaration: true option generates the .d.ts files, the exports map resolves them under every module resolution mode, the typesVersions is the legacy fallback, and the attw and publint tools verify the result. The publish uses the trusted publishing with the provenance, and the files allowlist keeps the package minimal. The types are the contract, and the publish is the delivery.


Stop using slow, ad-bloated tool sites! 🤮

🔎 Search “KandZ Tools” on Google to use many professional utilities for free.

KandZ.me is the ultimate minimalist hub for:
✅ Finance (Mortgage, Interest, Inflation)
✅ Tech (Base64, JSON, Dev Suite, IP)
✅ Health (BMI, BMR, TDEE)
✅ Productivity (Timer, Workspace, QR)

⚡️ Fast & Private
🔒 No data leaves your device
💎 100% Free

🔗 Use it now: https://tools.kandz.me
🔖 Bookmark it—you’ll need it later!