import { reactive, toValue } from 'vue'; import type { UnwrapRef } from 'vue'; import { isFunction } from '@robonen/stdlib'; /** * The reactive object produced by {@link reactivePick}: a subset of `T` * restricted to the picked keys `K`, with each value unwrapped. */ export type ReactivePickReturn = { [Key in K]: UnwrapRef }; /** * Predicate form: receive each `(value, key)` pair of the source object and * return `true` to keep the key in the resulting reactive view. */ export type ReactivePickPredicate = (value: T[keyof T], key: keyof T) => boolean; /** * @name reactivePick * @category Reactivity * @description Reactively pick a subset of keys (or keys matched by a * predicate) from a reactive object. The result is a live `reactive` proxy: * reads forward to the source's current value (so it tracks reassignment of * nested refs) and writes pass straight back to the source. Unlike a * `computed` of an object literal, no new object is allocated per recompute — * the proxy is built once and lookups are resolved lazily on access. * * @param {T} obj The reactive source object (a `reactive`, `ref` value, or plain object) * @param {...((K | K[]))} keys One or more keys (or arrays of keys) to pick * @returns {ReactivePickReturn} A reactive view limited to the picked keys * * @example * const state = reactive({ x: 1, y: 2, z: 3 }); * const picked = reactivePick(state, 'x', 'y'); * picked.x; // 1 — stays in sync with state.x * picked.x = 10; // writes back: state.x === 10 * * @example * // predicate form — keep only numeric values * const filtered = reactivePick(state, (value) => typeof value === 'number'); * * @since 0.0.14 */ export function reactivePick( obj: T, ...keys: Array ): ReactivePickReturn; export function reactivePick( obj: T, predicate: ReactivePickPredicate, ): ReactivePickReturn; export function reactivePick( obj: T, ...keys: Array> ): ReactivePickReturn { const first = keys[0]; // Predicate form: the set of kept keys is data-dependent, so it must be // evaluated on every key-set access (`ownKeys`/`has`/`get`). Value reads // still forward straight to the source, keeping the view reactive. if (isFunction(first)) { const predicate = first as ReactivePickPredicate; const keeps = (key: PropertyKey): boolean => key in obj && predicate(toValue((obj as Record)[key]) as T[keyof T], key as keyof T); return reactive(new Proxy({} as ReactivePickReturn, { get(_, key, receiver) { return keeps(key) ? Reflect.get(obj, key, receiver) : undefined; }, set(_, key, value) { return keeps(key) ? Reflect.set(obj as object, key, value) : true; }, has(_, key) { return keeps(key); }, ownKeys() { return Reflect.ownKeys(obj).filter(keeps); }, getOwnPropertyDescriptor(_, key) { if (!keeps(key)) return undefined; return { enumerable: true, configurable: true }; }, })) as ReactivePickReturn; } // Key form: flatten the (possibly nested) key arguments into a stable Set // once at construction time, then resolve membership in O(1) per access. const picked = new Set((keys as Array).flat()); return reactive(new Proxy({} as ReactivePickReturn, { get(_, key, receiver) { return picked.has(key) ? Reflect.get(obj, key, receiver) : undefined; }, set(_, key, value) { return picked.has(key) ? Reflect.set(obj as object, key, value) : true; }, has(_, key) { return picked.has(key) && key in obj; }, ownKeys() { return Reflect.ownKeys(obj).filter(key => picked.has(key)); }, getOwnPropertyDescriptor(_, key) { if (!picked.has(key) || !(key in obj)) return undefined; return { enumerable: true, configurable: true }; }, })) as ReactivePickReturn; }