Persistent collections and the ES2015 typing wall in Immutable.js
Immutable persistent data collections for Javascript which increase efficiency and simplicity.
At a glance
- What is it?
- Immutable.js gives JavaScript applications persistent List, Map, Set, OrderedMap, OrderedSet, Stack, Record and a lazy Seq that share untouched nodes between old and new values. It handles the copying for you, and it charges for that by taking away every mutating method, then adding a TypeScript definition set that demands an ES2015 lib.
- Who is it for?
- Immutable.js earns its place in a React or Flux application where state arrives from above and Immutable.is() decides what to re-renders. It does not suit code that must keep an older TypeScript lib, or a team that would rather write plain mutations against a draft.
- Can I use it commercially?
- Yes. MIT is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
- Is it still maintained?
- Yes. The repository last received commits 21 days ago.
- What is it written in?
- Mainly TypeScript, according to GitHub's language statistics.
Answers come from the project's GitHub data, last synced on September 30, 2026, and from our analysis. They are not legal advice.
Editorial analysis
set() hands back a second Map and leaves the first one alone
Calling `set()` never writes to the collection you called it on. The snippet in the README builds one Map from an object, derives a second with `set('b', 50)`, then prints both readings side by side so the difference is visible.
import { Map } from 'immutable';
const map1 = Map({ a: 1, b: 2, c: 3 });
const map2 = map1.set('b', 50);
map1.get('b') + ' vs. ' + map2.get('b'); // 2 vs. 50The copying you would otherwise write by hand is handled by hash array mapped tries and vector tries, the structures Clojure and Scala made popular. Nodes an edit does not touch are shared between the old collection and the new one, which is why the README points at minimizing the need to copy or cache data. Seven types sit on that base: `List`, `Stack`, `Map`, `OrderedMap`, `Set`, `OrderedSet` and `Record`.
Nothing in that base mutates. There is no append-in-place and no delete-in-place, so the moment a reducer or a component mutates a plain object or array nested inside your data, that code has to be rewritten to return a new value before the sharing buys you anything.
Four install commands, three published entry points
Four install paths are offered and all of them resolve to the same package name, `immutable`.
# using npm
npm install immutable
# using Yarn
yarn add immutable
# using pnpm
pnpm add immutable
# using Bun
bun add immutable`package.json` declares an engine constraint of npm 7.0.0 or higher, so a CI image pinned to npm 6 refuses the install outright rather than quietly resolving something older. The published fields split the build three ways: `dist/immutable.js` under `main`, `dist/immutable.es.js` under `module`, and `dist/immutable.d.ts` under `types`. A bundler reading `module` gets the ES build, while a plain require in Node takes the CommonJS file. Both carry the same collections, so the choice is about how the file loads rather than what it can do. The version sitting in the repository is 5.1.9.
The browser path is a script tag and one global named Immutable
The package has no dependencies, which is why including it in a browser is called predictable. A bundler such as webpack, rollup or browserify is recommended, and the `immutable` module works under one with no extra configuration. The fallback is a script tag pointing at `immutable.min.js` from a CDN such as CDNJS or jsDelivr, which adds `Immutable` to the global scope.
<script src="immutable.min.js"></script>
<script>
var map1 = Immutable.Map({ a: 1, b: 2, c: 3 });
var map2 = map1.set('b', 50);
map1.get('b'); // 2
map2.get('b'); // 50
</script>An AMD-style loader such as RequireJS accepts the same file through its own callback form. What the tag path gives up is the module boundary: one global `Immutable` for the whole page, and no way to hold two copies of the library under separate names. The docs are generated from `README.md` and `immutable.d.ts`, and a `website/` directory at the repository root holds the site they are built for, so a fork that needs different documentation starts by editing the source rather than the published HTML.
The bundled typings demand an ES2015 lib even where the runtime does not
Installing through npm brings type definitions for Flow v0.55.0 or higher and for TypeScript v4.5 or higher, so nothing extra is needed to get started. The v4 and later definitions take a different line from the runtime. They embrace ES2015, and while the library itself still supports legacy browsers and environments, its type definitions require TypeScript's 2015 lib. You must include either `"target": "es2015"` or `"lib": "es2015"` in your `tsconfig.json`, or hand `--target es2015` or `--lib es2015` to the `tsc` command.
That constraint is the first thing to check before committing. A codebase pinned to an older lib collects type errors from the definitions rather than from its own code, and raising the lib is a project-wide decision you cannot make from inside one dependency. Projects on v3 and earlier take a different route and pull in a reference file by relative path at the top of the file.
///<reference path='./node_modules/immutable/dist/immutable.d.ts'/>
import { Map } from 'immutable';
var map1: Map<string, number>;
map1 = Map({ a: 1, b: 2, c: 3 });
var map2 = map1.set('b', 50);
map1.get('b'); // 2
map2.get('b'); // 50No supported way of keeping an older lib with the newer definitions is described anywhere in the repository documentation.
The npm tarball is dist, README and LICENSE, and nothing more
The `files` array in `package.json` is short: `dist`, `README.md` and `LICENSE`. Everything else in the repository stays out of what npm installs. That excludes `src/`, so the implementation behind `Map` and `List` is not readable from `node_modules`, and it excludes `__tests__/`, so the behavioural cases the maintainers assert against are not there either. The `perf/`, `resources/` and `website/` directories are repository-only as well.
It also separates the typings you compile against from the source they are written in. The package points `types` at `dist/immutable.d.ts`, while the definition file the documentation is generated from lives at `type-definitions/immutable.d.ts` in the repository. Checking what a method signature means therefore means reading a generated artifact, and the editable source sits somewhere else. The CHANGELOG in the repository root is not part of the installed package either, so release notes have to be fetched from the repository rather than read out of your own lockfile.
A Seq defers the work, and no cache is documented for the second read
`Seq` is the lazy collection in the package. Chaining `map` and `filter` across it does not create an intermediate representation, and `Range` and `Repeat` are the constructors named for building one. The whole point is to hold a description of a pipeline instead of a finished result.
What is not written down is what happens the second time that Seq is read, and no memoization option appears anywhere in the documentation. Because the value on screen is a recipe rather than an answer, do not hand a single Seq to two components and count on the transform having run once. If you need something computed, comparable and safe to pass downward, take the eager `List` or `Map` instead.
That is the same rule the rest of the docs rest on: collections are treated as values rather than as objects, which is why `Immutable.is()` is the function the documentation tells you to reach for. An object stands for a thing that may change over time, a value stands for the state of that thing at one moment, and passing a new value down is the only change signal there is.
Three release lines shipped inside ten weeks
Three tags landed close together: v6.0.0-beta.1 on 2026-08-16, v3.8.4 on 2026-08-14 and v5.1.9 on 2026-06-29. The version in the repository's own `package.json` still reads 5.1.9, so the default branch is not tracking the newest tag. The last push to the repository was on 2026-09-10.
The upgrade question is the one to settle early. A beta of the next major and a patch release on a major three versions behind shipped in the same week, which means a range in your manifest can move you further than a routine install intends. No migration path between major versions is documented in the README, and the CHANGELOG is not shipped inside the package, so checking each call site falls to whoever is upgrading. Pin the line you have actually tested rather than accepting whatever the range resolves to on a build machine.
Immer inverts the rule: drafts instead of return-new-value discipline
Immer reaches the same goal from the other direction. Rather than a collection type that can only produce new values, you write the mutation you would have written against a plain object, operating on a draft copy, and the library hands back a new frozen value with the unchanged parts shared. Immutable.js makes no comparison of the two, so treat this as the difference the designs imply. Returning a new value is a convention you enforce by hand at every call site, including the ones you do not own. A draft is enforced by the thing you are holding.
That gap shows up in code you did not write. A library that mutates the object it is handed, a class instance stored as a Map value, or a hand-rolled reduce that pushes into an array: all of those have to be reshaped before the structural sharing does any work for you. `Record` covers the fixed-shape case, where named fields behave like a typed object, and the lazy `Seq` covers the case where you want a view over data rather than a second stored copy of it. The trade is real either way. Choose Immutable.js when your render layer already keys on reference identity. Choose a draft-based tool when it does not.
Editorial conclusion
Immutable.js earns its place in a React or Flux application where state arrives from above and Immutable.is() decides what to re-renders. It does not suit code that must keep an older TypeScript lib, or a team that would rather write plain mutations against a draft. Verify two things before adopting: whether your tsconfig.json can take an ES2015 lib, and which release line you intend to pin, since the main branch still reads 5.1.9 while v6.0.0-beta.1 and v3.8.4 shipped in the same week.
Frequently asked questions
What is Immutable.js?
Immutable.js provides persistent data collections for JavaScript, including List, Stack, Map, OrderedMap, Set, OrderedSet and Record, plus a lazy Seq. Data cannot be changed once created, and the mutative API yields new updated data rather than writing in place.
Is Immutable.js still used?
The last push to the repository was on 2026-09-10, and three tags were released in 2026: v5.1.9 on 2026-06-29, v3.8.4 on 2026-08-14 and v6.0.0-beta.1 on 2026-08-16. The project points contributors at a Slack workspace, a wiki of articles on specific topics, and its issue tracker.
Does Immutable.js need extra setup for TypeScript?
Installing it with npm brings type definitions for Flow v0.55.0 or higher and for TypeScript v4.5 or higher, so nothing extra is needed to begin. The v4 and later definitions require an ES2015 target or lib, either as "target": "es2015" or "lib": "es2015" in tsconfig.json, or as --target es2015 or --lib es2015 on the tsc command.
What does the Immutable.js npm package actually ship?
The files field limits the published tarball to dist, README.md and LICENSE, so src/ and __tests__/ stay in the repository only. main resolves to dist/immutable.js, module to dist/immutable.es.js and types to dist/immutable.d.ts.
What is a Seq in Immutable.js and when should one be avoided?
Seq is the lazy collection, so chaining map and filter across it creates no intermediate representation, and Range and Repeat are given as the ways to build one. The documentation says nothing about what happens when the same Seq is read twice and documents no memoization option, so use the eager List or Map when the value has to be computed once and compared with Immutable.is().
Official sources
Add this badge to your README
If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.
[](https://hysenlabs.com/projects/immutable-js-immutable-js)