Recipe · Frameworks and tests

How do I publish a JavaScript package with types?

  • Library (ships .d.ts)
  • package.json exports
  • .d.ts
  • npm
  • Checked with TypeScript 5.9

The problem I generate .d.ts files for my JavaScript package, but a project that installs it still gets 'Could not find a declaration file'. What does package.json need so that tsc and editors find the types?

The short answer

In package.json, give each entry in exports a types condition first, pointing at the .d.ts file, and include the types folder in files:

@acme/price-tools/package.json
{
  "name": "@acme/price-tools",
  "version": "1.0.0",
  "type": "module",
  "exports": {
    ".": {
      "types": "./types/index.d.ts",
      "default": "./src/index.js"
    }
  },
  "files": ["src", "types"]
}

A consumer project then gets the declared types, checked here with a jsconfig.json that does not read the package’s JavaScript at all (maxNodeModuleJsDepth: 0), so the types can only come from the .d.ts:

app.js app.ts TypeScript 5.9.3 TypeScript 5.9.3, run 21 October 2026: No errors. 6.0.3 TypeScript 6.0.3, run 21 October 2026: the same as 5.9.3. No errors. 7.0.2 TypeScript 7.0.2, run 21 October 2026: the same as 5.9.3. No errors. checked 21 October 2026
import { formatPrice } from "@acme/price-tools";

console.log(formatPrice(1999).padStart(10));
import { formatPrice } from "@acme/price-tools";

console.log(formatPrice(1999).padStart(10));
A consumer importing a package whose exports point at its types.
jsconfig.json for this example
{
  "compilerOptions": {
    "checkJs": true,
    "noEmit": true,
    "strict": true,
    "target": "es2022",
    "module": "nodenext",
    "types": [],
    "maxNodeModuleJsDepth": 0
  },
  "include": ["*.js"]
}

Why it works

Under module: "nodenext" (and node16 or bundler), tsc resolves a package import the way Node.js does: through exports. For each entry it looks for a types condition before default, and uses that file for the types while the runtime uses the JavaScript. The condition must come first, because conditions are matched in order. Writing the .d.ts files is a separate step, covered in creating .d.ts files, and the library setup puts both together.

The same thing in TypeScript

The TypeScript equivalent tab is the consumer as a .ts file; it resolves the package the same way. A package written in TypeScript publishes the same exports map, with its compiled JavaScript as default.

What goes wrong

A top-level types field is not enough once the package has exports: tsc follows exports, finds only JavaScript, and reports TS7016, even mentioning where the types are:

app.js Error confirmed 5.9.3 TypeScript 5.9.3, run 21 October 2026: app.js(2,29): error TS7016: Could not find a declaration file for module '@acme/price-tools'. '/project/node_modules/@acme/price-tools/src/index.js' implicitly has an 'any' type. There are types at '/project/node_modules/@acme/price-tools/types/index.d.ts', but this result could not be resolved when respecting package.json "exports". The '@acme/price-tools' library may need to update its package.json or typings. 6.0.3 TypeScript 6.0.3, run 21 October 2026: the same as 5.9.3. app.js(2,29): error TS7016: Could not find a declaration file for module '@acme/price-tools'. '/project/node_modules/@acme/price-tools/src/index.js' implicitly has an 'any' type. There are types at '/project/node_modules/@acme/price-tools/types/index.d.ts', but this result could not be resolved when respecting package.json "exports". The '@acme/price-tools' library may need to update its package.json or typings. 7.0.2 TypeScript 7.0.2, run 21 October 2026: the same as 5.9.3. app.js(2,29): error TS7016: Could not find a declaration file for module '@acme/price-tools'. '/project/node_modules/@acme/price-tools/src/index.js' implicitly has an 'any' type. There are types at '/project/node_modules/@acme/price-tools/types/index.d.ts', but this result could not be resolved when respecting package.json "exports". The '@acme/price-tools' library may need to update its package.json or typings. checked 21 October 2026
import { formatPrice } from "@acme/price-tools";error TS7016: Could not find a declaration file for module '@acme/price-tools'. '/project/node_modules/@acme/price-tools/src/index.js' implicitly has an 'any' type.
  There are types at '/project/node_modules/@acme/price-tools/types/index.d.ts', but this result could not be resolved when respecting package.json "exports". The '@acme/price-tools' library may need to update its package.json or typings.

console.log(formatPrice(1999));
The same package with types that exports does not lead to.
jsconfig.json for this example
{
  "compilerOptions": {
    "checkJs": true,
    "noEmit": true,
    "strict": true,
    "target": "es2022",
    "module": "nodenext",
    "types": [],
    "maxNodeModuleJsDepth": 0
  },
  "include": ["*.js"]
}

Gotchas

  • Every subpath needs its own types. An export such as "./format" needs its own types condition (or a .d.ts next to its .js file).
  • A .d.ts next to the .js file also works. For "default": "./src/index.js", tsc also looks for ./src/index.d.ts; a separate types/ folder needs the explicit condition.
  • Ship the files. If files in package.json leaves out types/, the published package has a types condition that points at nothing.

Questions

Is the top-level types field still needed?

Not for consumers that resolve with exports (node16, nodenext, bundler). It still helps tools and configs that ignore exports, such as moduleResolution: "node10", so packages often keep both, pointing at the same file.

How can I check my package before publishing?

Pack it with npm pack, install the tarball in a small consumer project with a jsconfig.json, and run tsc there. The "Are the types wrong?" tool from the TypeScript community checks the same thing for several resolution modes.

Sources

Last reviewed 21 October 2026.