// ============================================================================ // toSignal / toSignals - Per-property signal views over reactive objects // ============================================================================ import { markSignal } from './signal-brand'; /** * A signal-shaped live view over a single property of a reactive object. * Reads and writes delegate to the source, so reactivity is preserved. */ export type PropertySignal = { value: T }; /** * Property keys eligible for toSignal/toSignals: string keys excluding * `$set` (the object-signal's replace method injected by the proxy — not * data). Note that `toSignals()` additionally only creates views for keys * `Object.keys` yields at runtime (own enumerable keys). */ type SignalKey = Exclude, '$set'>; /** * Create a signal-shaped view over one property of a reactive object. * Unlike destructuring (which snapshots the value), the returned object * reads and writes through to the source, so tracking and triggering work. * * @example * ```ts * const state = signal({ count: 0 }); * const count = toSignal(state, 'count'); * count.value++; // triggers effects watching state.count * ``` */ export function toSignal>(source: T, key: K): PropertySignal { return markSignal({ get value() { return source[key]; }, set value(newValue: T[K]) { // Reflect.set reports rejected writes (read-only descriptors, // proxies that refuse the set) instead of a generic TypeError. if (!Reflect.set(source, key, newValue)) { // Prod ships the bare code + docs pointer; the full message is // dev-only (the __DEV__ branch folds away in the prod dist). throw new Error( __DEV__ ? `[sigx] toSignal: cannot write to read-only property "${String(key)}".` : 'SIGX500 — see https://sigx.dev/errors/SIGX500/' ); } } }); } /** * Per-key views of a reactive object. The homomorphic key-remapped map * preserves optional property markers from T; at runtime, views exist only * for keys Object.keys yields (own enumerable string keys). */ export type ToSignals = { [K in keyof T as K extends SignalKey ? K : never]: PropertySignal; }; /** * Create signal-shaped views for every own enumerable property of a reactive * object, so it can be destructured without losing reactivity. * * @example * ```ts * const state = signal({ count: 0, name: 'Ada' }); * const { count, name } = toSignals(state); * count.value++; // still reactive * ``` */ export function toSignals(source: T): ToSignals { const result = {} as ToSignals; for (const key of Object.keys(source)) { // Match the type-level contract at runtime: user data may contain an // own enumerable "$set" key, which must not get a view (the proxy get // trap would serve the injected replacer instead of the data). if (key === '$set') continue; (result as Record>)[key] = toSignal(source, key as SignalKey) as PropertySignal; } return result; }