The latest TS doesn’t support the types
key at the package.json root level. Paul Zaczkiewicz raised an issue on our GH tracker about it, and together we were able to come up with a repoduction (later I also discovered a related ticket on TS issues board). From there on, it was easy to solve; here’s how.
But first, some background. Before, the npm packages used to put types
in their package.json
root:
// package.json
{
"type": "module",
"exports": {
"script": "./dist/edit-package-json.umd.js",
"default": "./dist/edit-package-json.esm.js"
},
"types": "types/index.d.ts" // <--- here only
}
See the redux
package.json for example.
For years, we’ve been linking type definitions via the types
key at the package.json
root.
However, the projects running on TS 4.7+ with strict
enabled won’t recognise the imported npm package’s types if they are wired up this way. 😱
So, conceptually, the TypeScript maintainers at Microsoft changed the TypeScript API and it was worth of a major semver bump, not minor, and the whole world now has to update their Open Source programs to adhere to it. Including redux
, me and probably you too.
It’s not the first time and not only Microsoft is guilty doing it. So, we take a deep breath and go to update all our npm-bound programs. 🤷
How to fix
The types
now have to be inside exports
, and as the first entry.
Like this, adapted from official TS docs:
// package.json
{
"type": "module",
"exports": {
".": {
"import": {
"types": "./types/index.d.ts", // <-- here
"default": "./dist/edit-package-json.esm.js"
}
},
"./package.json": "./package.json"
},
"types": "types/index.d.ts"
}
or this, if you don’t need to allow a package.json
direct import:
// package.json
{
"type": "module",
"exports": {
"types": "./types/index.d.ts", // <-- add this
"script": "./dist/edit-package-json.umd.js",
"default": "./dist/edit-package-json.esm.js"
},
"types": "types/index.d.ts" // <-- still keeping for now
}
Also, I’m keeping the old “types” at the root level for now, they don’t start with ./
.
As always, feel free to reference our monorepo current setup, in case this article gets stale. Open any package’s root folder and check its package.json
.
Q&A
Why Codsen program type definitions are not in ./dist/
?
Distribution files (ESM and IIFE builds) are in ./dist/
, but type definitions are in ./types/
because each uses a different build tooling and it’s handy to keep them separate, for example you wipe the build artifacts and re-build by running one script, not two.
Another argument is, types are not relevant to UMD build (which is for browsers and browsers don’t support TS) and by keeping them two in the same folder we imply they are related somehow.
Type definitions are built using rollup
+ rollup-plugin-dts
from TS sources.
TS sources are built into ESM and IIFE using esbuild
. It’s not rollup
because esbuild
is faster. For the record, few years ago our packages-programs (not packages-CLI’s and not packages-plugins such as Gulp which are “different beasts” altogether) used rollup
to build everything (incl. TS transpiling), but not any more.
Why do you ship bundled UMD builds? Sandboxes such as StackBlitz made local IIFE bundles redundant, people would not bother using UMD as script, from CDN, inside HTML!
First, they do; people actually consume scripts from CDN:
Second, even on a React website, on a fancy full-stack Remix web application where one can npm i
the default ESM build of a dependency, the UMD build still has its place — web workers! You see, our program playgrounds such as this, this and this, are a bit CPU-bound and to offload the main thread and to make the UI responsive, we perform the calculations on a web worker. Now, the web worker can’t “tap” the program from node_modules
; it imports a .js
file, which must have all dependencies bundled in. Well, that’s the UMD spec: IIFE with all dependencies bundled.
Why don’t you use tsc
to build npm package type definitions?
Because typescript
CLI, the tsc
, doesn’t bundle the type definitions.
Why do you need to bundle npm package type definitions?
Because we use a monorepo.
When we ship a package from a monorepo’s subfolder to npm, all cross-references to the types from neighbour packages (outside formal dependencies as per package.json
) or parent folders (types placed on top levels for DRY purposes) break because when you npm i
, those folders “higher up” don’t exist in this new node_modules
location where user has installed our npm package.
Still not convinced? Here’s an actual, broken type definitions that I, Roy shipped on ranges-sort
v3.14.0 and it happened because I haven’t set up type definition bundling yet back then. That import { Ranges } from "../../../scripts/common";
would not resolve in consumer’s node_modules
. 😱 Lessons learned. But on the other hand, nobody tells you such things; the “civilian” Open Source package monorepos (different from 9-5 web app-oriented monorepos) on TypeScript is a very niche area.
Why do you use a monorepo?
To tackle the complexity as the npm package count grows. For example, one minute spent on each of 120 packages would be two hours, non-stop. But the same applies for one minute saved.