TS Deep Signals
Deep structural reactivity for plain objects / arrays / Sets built on top of alien-signals.
Hooks for Svelte (5 and 4), Vue, and React.
Core idea: wrap a data tree in a Proxy that lazily creates per-property signals the first time you read them. Deep mutations emit batched patch objects (in a JSON-patch inspired style) that you can track with watch().
Features
- Lazy: signals & child proxies created only when touched.
- Deep: nested objects, arrays, Sets proxied.
- Patch stream: microtask‑batched granular mutations (paths + op) for syncing external stores / framework adapters.
- Getter => computed: property getters become derived (readonly) signals automatically.
- Sets:
add/delete/clear/...methods emit patches; object entries get synthetic stable ids. - Configurable synthetic IDs: custom property generator - the synthetic ID is used in the paths of patches to identify objects in sets. By default attached as
@idproperty. - Read-only properties: protect specific properties from modification.
- Shallow escape hatch: wrap sub-objects with
shallow(obj)to track only reference replacement.
Install
pnpm add @ng-org/alien-deepsignals
# or
npm i @ng-org/alien-deepsignals
Quick start
import { deepSignal } from "@ng-org/alien-deepsignals";
const state = deepSignal({
count: 0,
user: { name: "Ada" },
items: [{ id: "i1", qty: 1 }],
settings: new Set(["dark"]),
});
state.count++; // mutate normally
state.user.name = "Grace"; // nested write
state.items.push({ id: "i2", qty: 2 });
state.settings.add("beta");
Frontend Hooks
We provide hooks for Svelte 4, Svelte 5, Vue, and React so that you can use deepSignal objects in your frontend framework. Modifying the object within those components works as usual, just that the component will rerender automatically when the object changed (by a modification in the component or a modification from elsewhere).
Note that you can pass existing deepSignal objects to useDeepSignal (that you are using elsewhere too, for example as shared state) as well as plain JavaScript objects (which are then wrapped).
You can (and are often advised to) use deepSignals as a shared state (and sub objects thereof) across components.
React
import { useDeepSignal } from "@ng-org/alien-deepsignals/react";
import { DeepSignal } from "@ng-org/alien-deepsignals";
import UserComponent from "./User.tsx";
import type { User } from "./types.ts";
function UserManager() {
const users: DeepSignal<User[]> = useDeepSignal([{ username: "Bob" }]);
return users.map((user) => <UserComponent key={user.id} user={user} />);
}
In child component User.tsx:
function UserComponent({ user }: { user: DeepSignal<User> }) {
// Modifications here will trigger a re-render in the parent component
// which updates this component.
// For performance reasons, you are advised to call `useDeepSignal`
// close to where its return value is used.
return <input type="text" value={user.name} />;
}
Vue
In component UserManager.vue
<script setup lang="ts">
import { useDeepSignal } from "@ng-org/alien-deepsignals/vue";
import { DeepSignal } from "@ng-org/alien-deepsignals";
import UserComponent from "./User.vue";
import type { User } from "./types.ts";
const users: DeepSignal<User[]> = useDeepSignal([{ username: "Bob", id: 1 }]);
</script>
<template>
<UserComponent v-for="user in users" :key="user.id" :user="user" />
</template>
In a child component, User.vue
<script setup lang="ts">
const props = defineProps<{
user: DeepSignal<User>;
}>();
// The component only rerenders when user.name changes.
// It behaves the same as an object wrapped with `reactive()`
const user = props.user;
</script>
<template>
<input type="text" v-model:value="user.name" />
</template>
Svelte 4
import { useDeepSignal } from "@ng-org/alien-deepsignals/svelte4";
// `users` is a store of type `{username: string}[]`
const users = useDeepSignal([{ username: "Bob" }]);
Svelte 5
import { useDeepSignal } from "@ng-org/alien-deepsignals/svelte";
// `users` is a rune of type `{username: string}[]`
const users = useDeepSignal([{ username: "Bob" }]);
Other Frameworks
Integrating new frontend frameworks is fairly easy. Get in touch if you are interested.
Reference
Interfaces
DeepPatchBatch
Defined in: sdk/js/alien-deepsignals/src/types.ts:23
Batched patch payload tagged with a monotonically increasing version.
Properties
patches
patches:
DeepPatch[]
Defined in: sdk/js/alien-deepsignals/src/types.ts:25
version
version:
number
Defined in: sdk/js/alien-deepsignals/src/types.ts:24
DeepSignalOptions
Defined in: sdk/js/alien-deepsignals/src/types.ts:42
Internal
Options to pass to deepSignal
Properties
propGenerator?
optionalpropGenerator:DeepSignalPropGenFn
Defined in: sdk/js/alien-deepsignals/src/types.ts:47
An optional function that is called when new objects are attached and that may return additional properties to be attached.
readOnlyProps?
optionalreadOnlyProps:string[]
Defined in: sdk/js/alien-deepsignals/src/types.ts:60
Optional: Properties that are made read-only in objects. Can only be attached by propGenerator or must already be member of the new object before attaching it.
replaceProxiesInBranchOnChange?
optionalreplaceProxiesInBranchOnChange:boolean
Defined in: sdk/js/alien-deepsignals/src/types.ts:66
If set to true, all proxies in the branch to a modified nested property are replaced.
This has no effect except for equality checks (===). This is necessary for react to notice the change.
Default
false;
subscriberFactories?
optionalsubscriberFactories:Set<ExternalSubscriberFactory<any>>
Defined in: sdk/js/alien-deepsignals/src/types.ts:71
syntheticIdPropertyName?
optionalsyntheticIdPropertyName:string
Defined in: sdk/js/alien-deepsignals/src/types.ts:54
The property name which should be used as an object identifier in sets.
You will see it when patches are generated with a path to an object in a set.
The syntheticId will be a patch element then.
Objects with existing properties matching syntheticIdPropertyName keep their values (not overwritten).
WatchOptions
Defined in: sdk/js/alien-deepsignals/src/watch.ts:27
Properties
immediate?
optionalimmediate:boolean
Defined in: sdk/js/alien-deepsignals/src/watch.ts:29
True, if the callback should be run immediately after watch was called.
Default
false;
once?
optionalonce:boolean
Defined in: sdk/js/alien-deepsignals/src/watch.ts:31
True, if the watcher should be unsubscribed after the first event.
Default
false;
triggerInstantly?
optionaltriggerInstantly:boolean
Defined in: sdk/js/alien-deepsignals/src/watch.ts:38
If true, triggers watch callback instantly after changes to the signal object. Otherwise, changes are batched and the watch callback is triggered in a microtask. This is useful for frontends like React where modifications on the changed input in a separate (microtask) will cause the cursor in input elements to reset.
WatchPatchEvent
Defined in: sdk/js/alien-deepsignals/src/watch.ts:41
Type Parameters
T
T extends object
Properties
newValue
newValue:
DeepSignal<T>
Defined in: sdk/js/alien-deepsignals/src/watch.ts:47
The current value of the signal
patches
patches:
DeepPatch[]
Defined in: sdk/js/alien-deepsignals/src/watch.ts:43
The changes made
version?
optionalversion:number
Defined in: sdk/js/alien-deepsignals/src/watch.ts:45
The version if triggerInstantly is not true.
Type Aliases
ComputedSignal
ComputedSignal<
T> =ReturnType<typeofcomputed>
Defined in: sdk/js/alien-deepsignals/src/types.ts:150
Type Parameters
T
T = any
DeepPatch
DeepPatch =
object& {op:"add";type?:"object"|"set";value?:any; } | {op:"remove";type?:"set";value?:any; }
Defined in: sdk/js/alien-deepsignals/src/types.ts:15
Deep mutation emitted from a deepSignal root.
Type Declaration
path
path: (
string|number)[]
DeepSignal
DeepSignal<
T> =TextendsFunction?T:Textendsstring|number|boolean?T:TextendsDeepSignalObjectProps<any> |DeepSignalObjectProps<any>[] ?T:Textends infer I[] ?DeepSignal<I>[] :TextendsSet<infer S> ?DeepSignalSet<S> :Textendsobject?DeepSignalObject<T> :T
Defined in: sdk/js/alien-deepsignals/src/types.ts:228
The object returned by the deepSignal function.
It is decorated with utility functions for sets, see DeepSignalSetProps
and a __raw__ prop to get the underlying non-reactive object.
Type Parameters
T
T
DeepSignalObject
DeepSignalObject<
T> ={ [K in keyof T]: DeepSignal<T[K]> }
Defined in: sdk/js/alien-deepsignals/src/types.ts:242
Type Parameters
T
T extends object
DeepSignalPropGenFn()
DeepSignalPropGenFn = (
props) =>object
Defined in: sdk/js/alien-deepsignals/src/types.ts:106
Internal
The propGenerator function is called when a new object is added to the deep signal tree.
Parameters
props
inSet
boolean
Whether the object is being added to a Set (true) or not (false)
object
any
The newly added object itself
path
(string | number)[]
The path of the newly added object.
Returns
extraProps?
optionalextraProps:Record<string,unknown>
Additional properties to be added to the object (overwriting existing ones).
syntheticId?
optionalsyntheticId:string|number
A custom identifier for the object (used in Set entry paths and optionally as a property).
Example
let counter = 0;
const state = deepSignal(
{ items: new Set() },
{
propGenerator: ({ path, inSet, object }) => ({
syntheticId: inSet
? `urn:item:${++counter}`
: `urn:obj:${path.join("-")}`,
extraProps: { createdAt: new Date().toISOString() },
}),
syntheticIdPropertyName: "@id",
},
);
state.items.add({ name: "Item 1" });
// Attaches `{ name: "Item 1", `@id`: "urn:item:1", createdAt: <current date>`
state.foo = { bar: 42 };
// Attaches `{bar: 42, "@id": "urn:obj:foo", createdAt: <current date>}`
DeepSignalSet
DeepSignalSet<
T> =DeepSignalSet_<T> &DeepSignalSetProps<T> &DeepSignalObjectProps<T>
Defined in: sdk/js/alien-deepsignals/src/types.ts:193
Type alias for DeepSignal<Set<T>> and reactive Set wrapper that accepts raw or proxied entries.
Additionally it is decorated with DeepSignalSetProps
and iterator utilities like .map(), .filter(), .some(), …
Note that you can assign plain Sets to properties with type DeepSignalSet,
however Typescript will give you a warning. That is a limitation of TypeScript’s capability.
Internally, the object will be converted to a DeepSignalSet.
You can instruct TypeScript to ignore this with parent.children = new Set() as DeepSignal<Set<any>>.
Type Parameters
T
T
DeepSignalSetProps
DeepSignalSetProps<
T> =object
Defined in: sdk/js/alien-deepsignals/src/types.ts:162
Utility functions for sets.
Type Parameters
T
T
Methods
first()
first():
Textendsobject?DeepSignal<T> :T|undefined
Defined in: sdk/js/alien-deepsignals/src/types.ts:164
Get the element that was first inserted into the set.
Returns
T extends object ? DeepSignal<T> : T | undefined
getBy()
getBy(
graphIri,subjectIri):DeepSignal<T> |undefined
Defined in: sdk/js/alien-deepsignals/src/types.ts:180
Retrieve an object from the Set by its @graph and @id.
Parameters
graphIri
string
The @graph NURI of the object.
subjectIri
string
The @subject IRI of the object.
Returns
DeepSignal<T> | undefined
The proxied entry if found, undefined otherwise.
getById()
getById(
id):DeepSignal<T> |undefined
Defined in: sdk/js/alien-deepsignals/src/types.ts:171
Retrieve an entry from the Set by its synthetic set ID.
Parameters
id
The synthetic ID (string or number) assigned to the entry.
string | number
Returns
DeepSignal<T> | undefined
The proxied entry if found, undefined otherwise.
ExternalSubscriberFactory()
ExternalSubscriberFactory<
T> = () =>object
Defined in: sdk/js/alien-deepsignals/src/types.ts:74
Type Parameters
T
T = any
Returns
object
onGet()
onGet: () =>
void
Returns
void
onSet()
onSet: (
newVal) =>void
Parameters
newVal
T
Returns
void
MaybeSignal
MaybeSignal<
T> =T|ReturnType<typeofalienSignal>
Defined in: sdk/js/alien-deepsignals/src/types.ts:249
Union allowing a plain value or a writable signal wrapping that value.
Type Parameters
T
T = any
MaybeSignalOrComputed
MaybeSignalOrComputed<
T> =MaybeSignal<T> | () =>T
Defined in: sdk/js/alien-deepsignals/src/types.ts:251
Union allowing value, writable signal, computed signal or plain getter function.
Type Parameters
T
T = any
RegisterCleanup()
RegisterCleanup = (
cleanupFn) =>void
Defined in: sdk/js/alien-deepsignals/src/watch.ts:25
Parameters
cleanupFn
() => void
Returns
void
SignalLike
SignalLike<
T> =WritableSignal<T> |ComputedSignal<T>
Defined in: sdk/js/alien-deepsignals/src/types.ts:151
Type Parameters
T
T = any
UnwrapDeepSignal
UnwrapDeepSignal<
T> =TextendsDeepSignal<infer S> ?S:T
Defined in: sdk/js/alien-deepsignals/src/types.ts:246
Type Parameters
T
T
WatchPatchCallback()
WatchPatchCallback<
T> = (event) =>void
Defined in: sdk/js/alien-deepsignals/src/watch.ts:50
Type Parameters
T
T extends object
Parameters
event
Returns
void
Variables
alienSignal()
constalienSignal: {<T>(): {():T|undefined; (value):void; }; <T>(initialValue): {():T; (value):void; }; } =alienSignal_
Defined in: sdk/js/alien-deepsignals/src/core.ts:99
Re-export of alien-signals signal function which creates a basic signal.
Call Signature
<
T>(): {():T|undefined; (value):void; }
Type Parameters
T
T
Returns
():
T|undefined
Returns
T | undefined
(
value):void
Parameters
value
T | undefined
Returns
void
Call Signature
<
T>(initialValue): {():T; (value):void; }
Type Parameters
T
T
Parameters
initialValue
T
Returns
():
T
Returns
T
(
value):void
Parameters
value
T
Returns
void
computed()
constcomputed: <T>(getter) => () =>T=alienComputed
Defined in: sdk/js/alien-deepsignals/src/core.ts:94
Re-export of alien-signals computed function.
Use the computed() function to create lazy derived signals that automatically
track their dependencies and recompute only when needed.
Key features:
- Lazy evaluation: The computation runs only when you actually read the computed value.
If you never access
fullName(), the concatenation never happens—no wasted CPU cycles. - Automatic caching: Once computed, the result is cached until a dependency changes. Multiple reads return the cached value without re-running the getter.
- Fine-grained reactivity: Only recomputes when its tracked dependencies change. Unrelated state mutations don’t trigger unnecessary recalculation.
- Composable: Computed signals can depend on other computed signals, forming efficient dependency chains.
Type Parameters
T
T
Parameters
getter
(previousValue?) => T
Returns
():
T
Returns
T
Example
import { computed } from "@ng-org/alien-deepsignals";
const state = deepSignal({
firstName: "Ada",
lastName: "Lovelace",
items: [1, 2, 3],
});
// Create a computed signal that derives from reactive state
const fullName = computed(() => `${state.firstName} ${state.lastName}`);
console.log(fullName()); // "Ada Lovelace" - computes on first access
state.firstName = "Grace";
console.log(fullName()); // "Grace Lovelace" - recomputes automatically
// Expensive computation only runs when accessed and dependencies change
const expensiveResult = computed(() => {
console.log("Computing...");
return state.items.reduce((sum, n) => sum + n * n, 0);
});
// No computation happens yet!
state.items.push(4);
// Still no computation...
console.log(expensiveResult()); // "Computing..." + result
console.log(expensiveResult()); // Cached, no log
state.items.push(5);
console.log(expensiveResult()); // "Computing..." again (dependency changed)
effect()
consteffect: (fn) => () =>void=alienEffect
Defined in: sdk/js/alien-deepsignals/src/core.ts:107
Re-export of alien-signals effect function.
Callback reruns on every signal modification that is used within its callback.
Parameters
fn
() => void
Returns
():
void
Returns
void
Functions
addWithId()
addWithId<
T>(set,entry,id):T
Defined in: sdk/js/alien-deepsignals/src/deepSignal.ts:1458
Convenience helper to add an entry to a proxied Set with a pre-defined synthetic ID.
Type Parameters
T
T
Parameters
set
Set<T>
entry
T
id
string | number
Returns
T
batch()
batch<
T>(fn):T
Defined in: sdk/js/alien-deepsignals/src/core.ts:34
Execute multiple signal writes in a single batched update frame. All downstream computed/effect re-evaluations are deferred until the function exits.
IMPORTANT: The callback must be synchronous. If it returns a Promise the batch will still end immediately after scheduling, possibly causing mid-async flushes.
Type Parameters
T
T
Parameters
fn
() => T
Returns
T
Example
batch(() => {
count(count() + 1);
other(other() + 2);
}); // effects observing both run only once
deepSignal()
deepSignal<
T>(input,specialOptions?):DeepSignal<T>
Defined in: sdk/js/alien-deepsignals/src/deepSignal.ts:1347
MAIN ENTRY POINT to create a deep reactive proxy for objects, arrays or Sets.
If input is a deepSignal already and options are provided,
the added subscriberFactories are joined with the existing ones
and replaceProxiesInBranchOnChange is or-ed with the current value.
Type Parameters
T
T extends object
Parameters
input
T
An object that you want to use as reactive, deepSignal object. If the input object is a DeepSignal object already, returns the same object.
specialOptions?
Additional configuration options.
Returns
DeepSignal<T>
A DeepSignal object.
You can use the returned DeepSignal as you would use your input object.
You do not have to to wrap your type definitions in DeepSignal<>.
Nevertheless, it can be instructive for TypeScript to show you the
additional utilities that DeepSignal objects expose. Also, it might keep
you aware that modifications you make to those objects are persisted and
can update the frontend.
The utilities that DeepSignal objects include are:
- For sets:
- iterator helper methods (e.g.
map(),filter(),reduce(),any(), …) first()to get one element from the set — useful if you know that there is only one.getBy(graphNuri: string, subjectIri: string), to find objects by their graph NURI and subject IRI.- NOTE: When assigning a set to
DeepSignal<Set>, TypeScript will warn you. You can safely ignore this by writing (parent.children = new Set() as DeepSignal<Set<any>>). Internally, the set is automatically converted but this is not expressible in TypeScript.
- iterator helper methods (e.g.
- For all objects:
__raw__which gives you the non-proxied object without tracking value access and without triggering updates upon modifications. Tracking value access is used in the frontend so it knows on what changes to refresh. If you use__raw__, that won’t work anymore.
Throws
if provided with unsupported input types.
getRaw()
getRaw<
T>(value):any
Defined in: sdk/js/alien-deepsignals/src/deepSignal.ts:1474
Get the original, raw value of a deep signal.
Type Parameters
T
T extends object
Parameters
value
T | DeepSignal<T>
Returns
any
isDeepSignal()
isDeepSignal(
value):value is any
Defined in: sdk/js/alien-deepsignals/src/deepSignal.ts:1314
Runtime guard that checks whether a value is a deepSignal proxy.
Parameters
value
unknown
Returns
value is any
shallow()
shallow<
T>(obj):T
Defined in: sdk/js/alien-deepsignals/src/deepSignal.ts:1445
Mark an object so deepSignal skips proxying it (shallow boundary).
Type Parameters
T
T extends object
Parameters
obj
T
Returns
T
Example
import { shallow } from "alien-deepsignals";
state.config = shallow({ huge: { blob: true } });
subscribeDeepMutations()
subscribeDeepMutations(
root,cb,triggerInstantly?): () =>void
Defined in: sdk/js/alien-deepsignals/src/deepSignal.ts:1397
Low-level function, you should probably use watch instead.
Register a deep mutation subscriber for the provided root or proxy.
Parameters
root
symbol | object
cb
DeepPatchJITSubscriber | DeepPatchSubscriber
triggerInstantly?
boolean = false
Returns
():
void
Returns
void
watch()
watch<
T>(source,callback,options?):object
Defined in: sdk/js/alien-deepsignals/src/watch.ts:89
Watch for changes to a deepSignal.
Whenever a change is made, callback is called with the patches describing the change and the new value.
If you set triggerInstantly, the callback is called on every property change.
If not, all changes are aggregated and callback is called in a microtask when
the current task finishes, e.g. await is called (meaning it supports batching).
When objects are added to Sets, their synthetic ID (usually @id) becomes part of the patch path. This allows patches to uniquely identify which Set entry is being mutated.
const state = deepSignal(
{ s: new Set() },
{ ...}
);
watch(state, ({ patches }) => {
console.log(JSON.stringify(patches));
});
state.s.add({ data: "test" });
// Will log:
// [
// {"path":["s","did:ng:o:123"],"op":"add","type":"object"},
// {"path":["s","did:ng:o:123","@id"],"op":"add","value":"did:ng:o:123"},
// {"path":["s","did:ng:o:123","data"],"op":"add","value":"test"}
// ]
state.s.getById("did:ng:o:123")!.data = "new value"
// Will log:
// [
// {"path":["s","did:ng:o:123","data"],"op":"add","value":"new value"}
// ]
Type Parameters
T
T extends object
Parameters
source
DeepSignalSet<T> | DeepSignalObject<T> | DeepSignal<T>
callback
options?
WatchOptions = {}
Returns
object
registerCleanup
registerCleanup:
RegisterCleanup
stopListening()
stopListening: () =>
void
Returns
void