ReadonlyView exposes a small public API for publishing a live, deeply readonly access path. See the ownership mental model for the owner/consumer boundary and guarantees and non-guarantees for the complete runtime contract.
function readonlyView<T>(source: T): DeepReadonly<T>;Creates an independent readonly membrane around a supported object-like source. It is for an owner that continues to mutate its source while consumers read the current graph without a mutation path through the returned view.
import { readonlyView } from '@nipe-solutions/readonly-view';
const source = { status: 'connecting' };
const view = readonlyView(source);
source.status = 'connected';
console.log(view.status); // connected- Reads observe the owner’s current source state.
- Writes through supported view paths throw
DirectMutationError. - Nested supported values are wrapped lazily, without an eager graph walk.
- Shared references and cycles keep stable identity within one membrane.
- The source is not intentionally frozen, sealed, copied, or otherwise mutated by wrapping.
Primitives are returned unchanged. Passing an existing ReadonlyView returns it unchanged. Each call with a non-view object creates an independent membrane, so wrappers from two calls are not identical even when they wrap the same source. Unsupported roots throw immediately; unsupported nested values throw only when consumer code reaches them. Consult supported types for the inventory and security and trust for alias and hostile-code limits.
function isReadonlyView(value: unknown): boolean;Recognizes views created by this package. Use it when an API needs to distinguish a ReadonlyView from an ordinary object before deciding whether to wrap or handle it specially.
import { isReadonlyView, readonlyView } from '@nipe-solutions/readonly-view';
const view = readonlyView({ enabled: true });
console.log(isReadonlyView(view)); // true
console.log(isReadonlyView({ enabled: true })); // falseIt returns true for package views, which readonlyView treats idempotently. It does not claim that an arbitrary proxy, frozen object, or TypeScript readonly value is a ReadonlyView.
The predicate is about the package’s runtime views, not structural typing. Primitives and unrelated objects return false.
class DirectMutationError extends Error {
readonly name: 'DirectMutationError';
readonly operation: string;
readonly property?: PropertyKey;
readonly objectKind: string;
constructor(details: {
readonly operation: string;
readonly property?: PropertyKey;
readonly objectKind: string;
});
}Signals that code attempted to mutate a supported value through a ReadonlyView. Catch it when mutation attempts are an expected boundary check; normally, letting it surface identifies an API misuse.
import {
DirectMutationError,
readonlyView,
} from '@nipe-solutions/readonly-view';
const view = readonlyView({ settings: { retries: 3 } });
try {
Reflect.set(view.settings, 'retries', 5);
} catch (error) {
if (error instanceof DirectMutationError) {
console.log(error.operation, error.property, error.objectKind);
}
}The error has name === 'DirectMutationError', an operation, an objectKind, and a property when that operation applies to a property. These fields are metadata for diagnostics; the source remains unchanged after the rejected write.
Collection and object operations can have different operation/property combinations, so treat property as optional. This error describes mutation through a supported view path, not mutation through an original source alias.
class UnsupportedTypeError extends Error {
readonly name: 'UnsupportedTypeError';
readonly kind: string;
constructor(kind: string);
}Signals that ReadonlyView cannot safely protect a native or otherwise unsupported value, or that a sensitive built-in exposes an unclassified native member.
import {
readonlyView,
UnsupportedTypeError,
} from '@nipe-solutions/readonly-view';
try {
readonlyView(/pattern/);
} catch (error) {
if (error instanceof UnsupportedTypeError) {
console.log(error.kind); // RegExp
}
}The error has name === 'UnsupportedTypeError' and a kind naming the unsupported value type or native member. Unsupported roots fail during wrapping; unsupported values deeper in an otherwise supported graph fail lazily when accessed. An unclassified Map, Set, or Date prototype member fails before its implementation is invoked with the mutable source.
Do not use this as a generic validation error. It is a deliberate fail-closed boundary for values and native operations that need dedicated handling. See the supported type matrix for the current list and rationale.
type DeepReadonly<T> = T extends Primitive
? T
: T extends abstract new (...arguments_: never[]) => object
? DeepReadonlyConstructor<T>
: T extends (...arguments_: never[]) => unknown
? DeepReadonlyFunction<T>
: T extends Date
? ReadonlyDate
: T extends readonly unknown[]
? { readonly [Key in keyof T]: DeepReadonly<T[Key]> }
: T extends ReadonlyMap<infer Key, infer Value>
? ReadonlyMap<DeepReadonly<Key>, DeepReadonly<Value>>
: T extends ReadonlySet<infer Value>
? DeepReadonlySet<Value>
: T extends object
? { readonly [Key in keyof T]: DeepReadonly<T[Key]> }
: T;This is the exported conditional type; its private helper aliases are omitted here for readability. See the source declaration for those helper definitions.
Models the compile-time shape returned by readonlyView. It recursively preserves primitives while presenting objects, arrays/tuples, Map, Set, and Date with readonly operations; nested results are readonly too.
import { readonlyView, type DeepReadonly } from '@nipe-solutions/readonly-view';
type PluginContext = { configuration: { retries: number } };
const context: DeepReadonly<PluginContext> = readonlyView({
configuration: { retries: 3 },
});
// @ts-expect-error nested property is readonly
context.configuration.retries = 5;DeepReadonly<T> is distributive across unions and retains optionals, nullability, symbols, recursive structures, readonly collection APIs, and deeply readonly function/constructor results. It models the supported runtime contract but does not itself add runtime enforcement.
TypeScript cannot generically preserve every overloaded or generic callable signature while transforming its return type. For complex callable APIs, publish a user-defined readonly interface at the boundary. A successful DeepReadonly<T> type also does not make an unsupported runtime value supported; rely on the supported types list for runtime behavior.