DEV Community

Cover image for Why JSR Should Be Your Default Registry for New TypeScript Libraries in 2026
Gabriel Anhaia
Gabriel Anhaia

Posted on

Why JSR Should Be Your Default Registry for New TypeScript Libraries in 2026


You sit down on a Sunday afternoon to publish a small TypeScript
helper. Two hundred lines of code, one dependency, four exports.
You want it on a registry by dinner.

Three hours later you have a tsup config, a package.json with
fifteen fields, an exports map you copied from a Stack Overflow
answer, a .d.ts rollup that still emits the wrong path on Bun,
two GitHub Actions secrets, and a CHANGELOG you swore you would
write later. The actual library is still two hundred lines.

That is the npm tax. It comes from a registry built for
CommonJS in 2010 absorbing ESM, types, dual exports, and ten
runtime targets on top. npm has more than a dozen ways to declare
an ESM entrypoint and most authors pick the wrong combination
for at least one runtime they meant to support.

JSR is the registry the Deno team
built to remove that tax.
In 2026 it has matured enough that for a brand new TypeScript
library (one with no existing npm history to protect), it should
be the default. This post is the case for why, with the runnable
shape of the workflow, and the cases where npm still wins.

The core idea: publish your source files

The npm model is that you write TypeScript, you compile it to
JavaScript plus a .d.ts declaration file, you ship the pair,
and the consumer's tooling consumes the .d.ts for type info and
the .js for runtime. The build step is where most of the pain
lives. ESM versus CJS, declaration map paths, conditional
exports, the types field versus the exports.types field, the
moduleResolution mode the consumer happens to use.

JSR runs the build for you. You publish the TypeScript source;
the registry generates type declarations and transpiles to
JavaScript for runtimes that need it. Your package.json does
not need an exports map, your repo does not need a dist/
directory, and there is no build step the publisher owns.

JSR's own docs are explicit about
this: the registry encourages publishing TypeScript source
rather than pairs of .js + .d.ts files because it lets JSR
generate documentation and improves editor auto-completion
across runtimes.

That single design choice removes most of the configuration
files I listed in the opening. The two-hundred-line library stays
two hundred lines plus a metadata file.

The publishing workflow, end to end

A complete JSR publish for a Deno-or-Node library is three files
and one command. The three files:

// jsr.json
{
  "name": "@gabriel/clock",
  "version": "0.1.0",
  "exports": "./mod.ts",
  "publish": {
    "include": ["mod.ts", "README.md", "LICENSE"]
  }
}
Enter fullscreen mode Exit fullscreen mode
// mod.ts
export interface Clock {
  now(): Date;
}

export const systemClock: Clock = {
  now: () => new Date(),
};

export function fixedClock(date: Date): Clock {
  return { now: () => date };
}
Enter fullscreen mode Exit fullscreen mode
<!-- README.md -->
# @gabriel/clock

A tiny Clock interface for swappable time in tests.
Enter fullscreen mode Exit fullscreen mode

The publish command, on Deno:

deno publish
Enter fullscreen mode Exit fullscreen mode

On Node or Bun, with no Deno install:

npx jsr publish
Enter fullscreen mode Exit fullscreen mode

That is the entire build pipeline: no tsc, no bundler, no
exports matrix. The registry reads the source, generates
.d.ts files for npm-compat consumers, transpiles to JavaScript
for runtimes that ask, and serves the original TypeScript to
Deno and Bun
(npm-compatibility docs
walk through this end to end). A consumer on Node 24 gets
generated .d.ts files plus JavaScript; a consumer on Deno gets
the TypeScript source directly.

The jsr.json schema is small. name is your scoped package,
version is semver, exports is either a single entrypoint
string or a record of subpaths to files, and publish.include
controls which files end up in the published tarball. That
covers most of the schema; the rest is license-detection and
CI overrides.

Installing on every runtime

The runtime story is the part that surprised me when I first ran
the matrix. The same @scope/pkg works on Deno, Node, Bun,
Cloudflare Workers, and any bundler that supports ES modules.

Deno (first-class):

deno add jsr:@gabriel/clock
Enter fullscreen mode Exit fullscreen mode
import { systemClock } from "@gabriel/clock";
Enter fullscreen mode Exit fullscreen mode

Node (via the npm-compat layer):

npx jsr add @gabriel/clock
Enter fullscreen mode Exit fullscreen mode
import { systemClock } from "@gabriel/clock";
Enter fullscreen mode Exit fullscreen mode

Bun:

bunx jsr add @gabriel/clock
Enter fullscreen mode Exit fullscreen mode

pnpm 10.9+ and Yarn 4.9+ have
first-class JSR support,
so the npx step is not needed:

pnpm add jsr:@gabriel/clock
yarn add jsr:@gabriel/clock
Enter fullscreen mode Exit fullscreen mode

The npx jsr add command is a thin shim. It writes a
jsr:@scope/pkg reference into your package.json, configures
the .npmrc to point JSR's npm-compat endpoint at the
@scope namespace, and runs your normal package manager from
there. After the first run, regular npm install keeps working
in the same project — your node_modules/@gabriel/clock is a
valid Node package with .d.ts files generated by JSR. Pure
npm-tooling consumers see something that looks like any other
npm package; the only difference is where it came from.

The official compatibility doc is the source of truth on the
exact set of supported package managers and the version floors.
npm compatibility on JSR
is the one to bookmark.

What you stop doing

The things you delete from a new TypeScript library when you
publish to JSR instead of npm fall into two groups: the build
pipeline, and the dual-format ceremony.

The build pipeline goes first. You drop the tsup / tsc /
unbuild / bun build config, the dist/ directory and its
.gitignore entry, and the prepublishOnly script that runs
the build before publish. You also drop the CI step that runs
tsc --noEmit against the built output to prove the .d.ts
files agree with the .js files. JSR generates them in lockstep
from the same source, so they cannot disagree.

The dual-format ceremony goes next. You drop the exports map
with its import / require / types triples, the types
and main and module fields, and the "is the consumer on
ESM or CJS today" branch in your README. The release-please
or changeset config can stay if you use it for changelog
generation, but you do not need it for registry mechanics.

The replaced pieces are one jsr.json file and one
deno publish (or npx jsr publish) call. That is the trade.

Where npm still wins in 2026

JSR is not a universal replacement. There are five cases where
I would still go npm-first (and skip JSR or treat it as a mirror).

If your library wraps a node-gyp binding or node-addon-api,
stay on npm. JSR's TypeScript-source model has no story for
native binaries, and npm's prebuild ecosystem (prebuildify,
node-pre-gyp, @mapbox/node-pre-gyp) handles runtime-specific
artifacts end-to-end. Anything with a .node file in the
shipped package is in this bucket.

Tooling that has not caught up is the second case. Some test
runners, doc generators, and Renovate-style update bots still
treat jsr:@scope/pkg references as second-class. Most resolve
them because npm-compat works, but a few hand back warnings or
fail to dedupe across the JSR/npm boundary. If your library
exists to plug into one of those tools, ship to npm where the
integration already works and add JSR once the tool catches up.

Large CI matrices are a quieter cost. Old CI images with Node 16
or Node 18 in the matrix get the npm-compat path, which works
but adds the JSR shim to every install. The added install time
across a twenty-job matrix is real. New libraries that need a
Node-16 row are rare in 2026, but if you are one of them, the
math is different.

The fourth case is a package name you already own on npm. If
@your-org/foo is on npm with twenty thousand weekly downloads,
do not move it. Dual-publish (next section) and let new releases
flow to both. Do not break the npm install for existing
consumers to chase a nicer dev workflow.

Pricing edge cases close the list. JSR is free for public
packages, and the private-package tier (as of April 2026) is in
early stages compared to npm's mature private-registry options.
If your library is private from day one, npm Enterprise, GitHub
Packages, or a self-hosted Verdaccio will probably fit your
security review more cleanly than the JSR private path.

For everything else, JSR is the path with the fewest config
files between you and a published package: a new public
TypeScript library, a small helper, an internal-but-open-source
utility, a pure-TS port of an existing C library, an SDK you
are about to publish for the first time.

The dual-publish pattern, when you want both

For libraries with an existing npm presence, the path forward is
publishing to both registries from the same source. JSR primary,
npm mirror.

The shape of the GitHub Actions workflow:

# .github/workflows/publish.yml
name: publish

on:
  push:
    tags: ["v*"]

permissions:
  contents: read
  id-token: write  # OIDC token for JSR

jobs:
  jsr:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: denoland/setup-deno@v2
        with: { deno-version: v2.x }
      - run: deno publish

  npm:
    runs-on: ubuntu-latest
    needs: jsr
    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       # your tsc / tsup step
      - run: npm publish --provenance --access public
        env:
          NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
Enter fullscreen mode Exit fullscreen mode

The JSR job uses OIDC, so no token secret is required. The npm
job runs after JSR succeeds and publishes the same source
through your normal tsc build, with --provenance for
supply-chain attestations. The version bump lives in
jsr.json; a small script in npm run build (or a separate
version-sync step) copies it to package.json before
npm publish, so both registries land on the same number.

There is exactly one place to bump the version when you cut a
release: jsr.json. The package.json value is derived from
it.

What it feels like in day-to-day authoring

The clearest signal that JSR-first changes the loop is what the
edit-publish cycle looks like for a one-line patch.

On npm:

  1. Edit the source file.
  2. Run the build script.
  3. Bump the version in package.json.
  4. Run the build script again because your prepublish did not pick up the version change.
  5. Stage the new dist/ files. Or do not, depending on your .gitignore policy.
  6. npm publish.
  7. Wait. Check the registry. Test the install in a fresh project.

On JSR:

  1. Edit the source file.
  2. Bump the version in jsr.json.
  3. deno publish.
  4. The install is live.

A friend who maintains a JSR-first lib pushes patches the same
evening a bug report comes in; the same person on the npm side
waits for the weekend, because the build step is where the
ceremony lives.

Pick the default that fits 2026

For a new TypeScript library, the burden of proof has flipped:
publish to JSR unless you have a specific reason on the list
above. Next time you write a library, start a jsr.json, run
deno publish, and see what the loop feels like without a build
step.


If this was useful

Library authoring across runtimes is what TypeScript in
Production
covers end to end — tsconfig for libraries, the
exports map, dual ESM/CJS for the cases where you still need
it, JSR publishing, and the failure modes that show up only when
a consumer is on a runtime you did not test against. If the
JSR pipeline above looked like a relief and you want the
production-side picture, that is the book.

If you are coming into TypeScript fresh and the type system
itself is the friction, TypeScript Essentials is the entry
point. The TypeScript Type System is the deep dive on
generics, conditional types, and the infer machinery that
library-grade typings depend on. The JVM and PHP bridge books
cover the same ground from those starting points.

The five-book set:

  • TypeScript Essentials — From Working Developer to Confident TS, Across Node, Bun, Deno, and the Browser — entry point: amazon.com/dp/B0GZB7QRW3
  • The TypeScript Type System — From Generics to DSL-Level Types — deep dive: amazon.com/dp/B0GZB86QYW
  • Kotlin and Java to TypeScript — A Bridge for JVM Developers — bridge for JVM devs: amazon.com/dp/B0GZB2333H
  • PHP to TypeScript — A Bridge for Modern PHP 8+ Developers — bridge for PHP devs: amazon.com/dp/B0GZBD5HMF
  • TypeScript in Production — Tooling, Build, and Library Authoring Across Runtimes — production layer: amazon.com/dp/B0GZB7F471

All five books ship in ebook, paperback, and hardcover.

The TypeScript Library — the 5-book collection

Top comments (0)