Expose live internal data without exposing mutation.
ReadonlyView adds runtime enforcement and TypeScript
types without cloning or freezing.
npm install @nipe-solutions/readonly-view
owner.tspublic.ts
Mutable source
source.user.name = 'Bob'
Readonly view
view.user.name = 'Eve'
LiveOwner changes appear immediately.LazyNested proxies exist only when read.DeepEvery supported exposure route is wrapped.v2Zero runtime dependencies. ESM and CommonJS.
Why this exists
An SDK often owns mutable connection state while
applications need to inspect that state without
rewriting it. Keep the source private and publish a
live readonly view.
import { readonlyView } from '@nipe-solutions/readonly-view'
class Client {
#state = { connected: false }
readonly state = readonlyView(this.#state)
connect() {
this.#state.connected = true
}
}
const client = new Client()
const state = client.state
client.connect()
state.connected // true
Reflect.set(state, 'connected', false) // throws
ReadonlyView removes mutation capability from the
view. The owner must still control any other mutable
aliases.
Use ReadonlyView at an ownership boundary where
consumers need current data but should not gain a
mutation path through the public view.
SDK public state
Connection state stays current after the SDK
connects, disconnects, or receives new data.
Applications can inspect it but cannot
rewrite it through state.
Internal registry
The registry keeps adding entries through
its own API. Search and rendering code sees
the current Map without access
to mutating collection methods.
Plugin context
A host can update source configuration while
plugins receive a live context that rejects
accidental nested configuration writes.
If ReadonlyView helps you share live data safely, a
GitHub star helps other developers discover it.
Explore more tools from NIPE Open Source for your
next project.
Public writes need TypeScript guidance
and runtime rejection.
Lazy wrapping is preferable to eager
traversal or copying.
Not a fit
You need a point-in-time snapshot.
You need structural sharing or new-state
production.
You need to prevent the owner from
mutating the source.
You need a security sandbox for hostile
JavaScript or other mutable aliases.
Introduction
ReadonlyView gives consumers a stable read surface
while the owner keeps the ordinary mutable source.
It is a membrane, not state management.
const view = readonlyView(source)
Ownership model
Keep the source private. Export the view. Mutate
only through the source reference; existing views
observe replacement, addition, deletion, collection
changes, and Date changes.
Examples
Each example uses the public API and can be copied
into a modern JavaScript project. TypeScript rejects
the mutation lines before they reach runtime.
import { readonlyView } from '@nipe-solutions/readonly-view'
Properties, symbols, getters, descriptors, indexes,
callbacks, spread, and iteration all pass exposed
values through the same membrane. Array identity is
preserved with Array.isArray(view).
Map and Set
Reads, keys, values, entries, iteration, size, and
forEach work with wrapped values. set,
add, delete, and
clear throw. A view key from the same
membrane remains usable for lookup.
Date
Native reads use a valid Date receiver. Every
available set* method is blocked. The
source Date remains mutable and live.
Functions and custom classes
User methods receive the readonly receiver, so
this.balance -= amount throws. A
function that closes over the source can still
mutate it. Private fields may fail their native
brand check because rebinding to the mutable source
would be unsafe. Prototype reflection is protected,
so class views intentionally do not preserve
instanceof.
Circular references and shared identity
Within one membrane, one source maps to one
view—even across properties, arrays, Map, Set, and
cycles. Separate top-level calls create independent
membranes.
Symbols and property descriptors
Own keys, symbols, membership, enumeration,
descriptors, accessors, spread, and destructuring
behave live. Descriptor values are views, never raw
nested sources.
Proxy invariants
Controlled extensible shadow targets avoid the
non-configurable-property escape inherent in
directly proxying arbitrary sources. View
descriptors describe the virtual view while source
descriptors remain untouched.
Initial creation wraps one value. Reads carry proxy
overhead; collection adapters allocate protected
iterator results. Size and methodology are checked
in the repository without performance marketing
claims.
Security and trust model
No evaluation, serialization, source writes, or
eager getter calls. Consumer proxies and arbitrary
user code remain trusted inputs. Mutable
backing-memory types are rejected.
API reference
readonlyView(source)
Create a deeply readonly live view.
isReadonlyView(value)
Recognize package views.
DirectMutationError
Mutation metadata and safe message.
UnsupportedTypeError
Rejected type kind.
DeepReadonly<T>
Public recursive readonly type.
Migration from ImmuView v1
// v1
const state = readonly(data)
state.internalSet(next)
// v2
const source = data
const state = readonlyView(source)
source.count += 1
The package is now
@nipe-solutions/readonly-view. The
wrapper .value, validation, deep merge,
and mutation lifecycle are gone.
FAQ
Is this Object.freeze?
No. The source remains mutable and the view
stays live.
Why reject types?
Explicit failure is safer than accidental native
mutation access.
Contributing
Use Node 24 and run npm run check. Add
a failing regression test before each bug fix.