I've published TypeScript packages that passed all my tests, built cleanly, and had types resolving perfectly in my editor then broke for consumers.
The bug was always in the exports field. A require condition pointing to an ESM file. The "types" key in the wrong position. A subpath missing its .d.ts entirely. These failures are silent until someone files an issue.
After debugging the same class of problem for the third time, I decided to automate the checks.
What goes wrong
1. CJS/ESM format mismatch
Your package.json says "require": "./dist/cjs/index.cjs". But that file contains import and export statements it's actually ESM. Works fine in Webpack and esbuild (they don't care). Crashes in Node.js with ERR_REQUIRE_ESM.
This happens because build tools sometimes output ESM syntax even when targeting CJS, especially with certain configurations. You'd never know unless you manually read the output file.
2. Wrong condition ordering
{
".": {
"import": "./dist/index.js",
"types": "./dist/index.d.ts",
"default": "./dist/index.js"
}
}
Spot the bug? "types" should be first. TypeScript resolves conditions top to bottom and stops at the first match. Here it matches "import" first, never sees "types", and falls back to inferring types from the JS file everything becomes any.
This one has bitten me multiple times because the exports field looks correct.
3. Missing declarations for subpaths
Your main entry has index.d.ts. But your package exports 20 subpaths:
{
"./utils": "./dist/utils.js",
"./hooks": "./dist/hooks.js"
}
If dist/utils.d.ts doesn't exist, TypeScript consumers importing from your-package/utils get any or "cannot find module." And you'll never know unless someone reports it, because your own project's tests use the source files directly.
The tool
I built tspub to catch these. It checks 70 rules across exports, types, files, metadata, imports, and package size.
$ npx @tspub-dev/tspub check
exports/format-mismatch ./dist/cjs/index.cjs contains ESM syntax
exports/types-first "types" should be first in conditions
types/no-any-export ./dist/index.d.ts exports 12 `any` types
3 problems (1 auto-fixable)
Some rules I haven't seen in other tools:
-
exports/cjs-esmodule-interopdetects CJS files using the__esModuleflag that causes different behavior across Webpack, Node, and esbuild -
types/no-any-exportflags declaration files where your exported types contain excessiveany(usually a build tool misconfiguration, not intentional) -
exports/format-mismatchactually reads file contents to verify format, not just the extension -
files/sensitivecatches.env, private keys, or credentials accidentally included in the published package
You can also auto fix certain issues:
$ npx @tspub-dev/tspub check --fix
This handles safe fixes like condition reordering.
Check any package without installing
I built a web version too: visit tspub.dev/check/YOUR-PACKAGE to check any npm package instantly.
Beyond checking
tspub also handles building (esbuild-based ESM/CJS + .d.ts), type declaration tests (.test-d.ts files), scaffolding new packages, and publishing to npm.
tspub init # scaffold a correctly-configured package
tspub build # ESM + CJS + .d.ts generation
tspub check # 70-rule validation
tspub test-types # type-level test runner
tspub publish # npm + GitHub releases
I built it as one tool because managing separate configs for building, linting, type testing, and publishing across multiple packages was getting unsustainable for me. Your mileage may vary the checking works standalone even if you don't use the build or publish features.
It's still early and I'd genuinely appreciate feedback:
- Does it catch real issues in your packages?
- False positives?
- Missing rules?

Top comments (0)