constancy
v2.0.0
Published
Lightweight utility to make objects and arrays immutable with shallow and deep freeze
Downloads
174
Maintainers
Readme
Constancy
Lightweight, zero-dependency TypeScript utility for making objects and arrays deeply immutable. Shallow and recursive freeze with full type safety.
Installation
npm install constancy
# or
yarn add constancy
# or
pnpm add constancyRequires Node.js >= 14.
Quick Start
import constancy, { deepFreeze } from 'constancy';
// Shallow freeze — top-level properties only
const config = constancy({ host: 'localhost', port: 3000 });
config.port = 8080; // Ignored (TypeError in strict mode)
// Deep freeze — all nested properties recursively
const state = deepFreeze({
user: { name: 'Alice', roles: ['admin'] },
settings: { theme: 'dark' },
});
state.user.name = 'Bob'; // Ignored
state.user.roles.push('user'); // IgnoredAPI Reference
constancy<T>(val: T)
Shallow-freezes an object or function using Object.freeze(). Primitives are returned unchanged.
Returns: Readonly<T> for objects/functions, T for primitives.
import constancy from 'constancy';
constancy({ a: 1 }); // Readonly<{ a: number }>
constancy([1, 2, 3]); // readonly number[]
constancy(() => 'hello'); // Frozen function (still callable)
constancy('text'); // 'text' — unchanged
constancy(42); // 42 — unchanged
constancy(null); // null — unchangedNested objects are NOT frozen:
const obj = constancy({ user: { name: 'Alice' } });
obj.user.name = 'Bob'; // Works — nested is not frozen
obj.user = {}; // Ignored — top-level is frozendeepFreeze<T>(val: T)
Recursively freezes a value and all reachable nested objects. Handles:
- Circular references (via WeakSet — already-visited nodes are skipped)
- Symbol-keyed properties (via
Reflect.ownKeys()) - TypedArrays (skipped — freezing non-empty TypedArrays throws natively)
- Accessor descriptors (
get/set) — skipped to avoid unintended invocation
Returns: DeepReadonly<T> for objects, T for primitives.
import { deepFreeze } from 'constancy';
const obj = deepFreeze({
nested: { count: 0 },
tags: ['a', 'b'],
});
obj.nested.count = 1; // Ignored
obj.tags.push('c'); // Ignored
// Circular references are safe
const node: any = { value: 1 };
node.self = node;
deepFreeze(node); // No infinite loopTypes
DeepReadonly<T>
Recursively marks all properties as readonly. Handles objects, arrays, Maps, and Sets.
import type { DeepReadonly } from 'constancy';
type Config = DeepReadonly<{
db: { host: string; port: number };
tags: string[];
}>;
// Config.db.host is readonly, Config.tags is ReadonlyArray<string>Freezable
Union type of values that can be frozen: object | Function.
import type { Freezable } from 'constancy';
function freeze(val: Freezable) {
return Object.freeze(val);
}TypeScript Usage
Named and default imports are both supported:
// Default import
import constancy from 'constancy';
// Named imports
import { constancy, deepFreeze } from 'constancy';
import type { DeepReadonly, Freezable } from 'constancy';CommonJS:
const { constancy, deepFreeze } = require('constancy');Migration from v1
Breaking changes
| v1 | v2 |
|---|---|
| Node.js >= 10 | Node.js >= 14 required |
| CommonJS-first (index.js) | ESM-first ("type": "module") |
| Object.freeze polyfill included | Removed — native in all supported environments |
| import constancy from 'constancy' | Same — default import still works |
| No deepFreeze | deepFreeze() added |
| No DeepReadonly<T> | Type exported |
Import changes
v1 root entry (index.js, index.d.ts) is removed. Imports via the package name continue to work through the exports field:
// Still works in v2
import constancy from 'constancy';
import { constancy, deepFreeze } from 'constancy';Direct path imports (from 'constancy/dist/...') are not supported.
Development
# Build ESM + CJS bundles
npm run build
# Run tests
npm test
# Watch mode
npm run test:watch
# Coverage report
npm run test:coverage
# Type check
npm run typecheckProject structure
src/
├── index.ts — barrel exports
├── constancy.ts — shallow freeze
├── deep-freeze.ts — recursive deep freeze
├── types.ts — DeepReadonly<T>, Freezable
└── utils.ts — isFreezable(), getOwnKeys()
tests/
├── constancy.test.ts — 19 tests
└── deep-freeze.test.ts — 11 tests
dist/ — built output (ESM + CJS + .d.ts)Why Constancy?
- Zero dependencies — no supply chain risk, no bloat
- Tiny — under 1KB minified + gzipped
- Type-safe —
Readonly<T>andDeepReadonly<T>inferred automatically - Safe — handles circular refs, Symbol keys, TypedArrays, getters
- Dual format — ESM + CJS with conditional exports
- Well-tested — 30 tests, ~97% coverage
License
MIT — DungGramer
