From fdcdb00a77183596fb63d3cbc28e6bf3a5b363a8 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 20 Jan 2026 09:51:40 +0000 Subject: [PATCH 1/2] Initial plan From 685201dbcf3ae3079aff59c289e97702e5fddb07 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 20 Jan 2026 10:51:09 +0000 Subject: [PATCH 2/2] Refine useProperties typing and docs Co-authored-by: spearwolf <12805+spearwolf@users.noreply.github.com> --- packages/shadow-objects/CHANGELOG.md | 1 + .../02-guides/02-creating-shadow-objects.md | 10 +++--- .../docs/03-api/01-shadow-object-api.md | 10 ++++-- .../docs/03-api/03-view-components.md | 9 +++-- .../docs/04-patterns/best-practices.md | 4 +-- .../src/in-the-dark/Kernel.spec.ts | 36 +++++++++++++++++++ .../shadow-objects/src/in-the-dark/Kernel.ts | 18 +++++----- packages/shadow-objects/src/types.ts | 4 ++- 8 files changed, 69 insertions(+), 23 deletions(-) diff --git a/packages/shadow-objects/CHANGELOG.md b/packages/shadow-objects/CHANGELOG.md index cc89bac..eaaeef6 100644 --- a/packages/shadow-objects/CHANGELOG.md +++ b/packages/shadow-objects/CHANGELOG.md @@ -8,6 +8,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## unreleased - sharpen the `EntityApi` type definitions +- improve `useProperties()` type inference with key-to-type maps - **Documentation:** Comprehensive update to the documentation structure and content. - Added dedicated documentation for Web Components (``, ``, ``) at `docs/03-api/04-web-components.md`. - Clarified the usage of Component Contexts, Namespacing (`ns` attribute), and decoupled placement of View Components. diff --git a/packages/shadow-objects/docs/02-guides/02-creating-shadow-objects.md b/packages/shadow-objects/docs/02-guides/02-creating-shadow-objects.md index 671bc4a..07270e1 100644 --- a/packages/shadow-objects/docs/02-guides/02-creating-shadow-objects.md +++ b/packages/shadow-objects/docs/02-guides/02-creating-shadow-objects.md @@ -9,13 +9,15 @@ The recommended way to define a Shadow Object is a simple function. This functio ```typescript import { ShadowObjectCreationAPI } from "@spearwolf/shadow-objects"; -export function UserProfileLogic({ - useProperty, - createEffect +export function UserProfileLogic({ + useProperties, + createEffect, }: ShadowObjectCreationAPI) { // 1. Setup Phase: Define your reactive graph here - const userId = useProperty('userId'); + const { userId } = useProperties<{userId: string}>({ + userId: 'userId', + }); createEffect(() => { // 2. Runtime Phase: This runs whenever userId changes diff --git a/packages/shadow-objects/docs/03-api/01-shadow-object-api.md b/packages/shadow-objects/docs/03-api/01-shadow-object-api.md index dd7304e..ad6360f 100644 --- a/packages/shadow-objects/docs/03-api/01-shadow-object-api.md +++ b/packages/shadow-objects/docs/03-api/01-shadow-object-api.md @@ -39,12 +39,16 @@ createEffect(() => { A convenience helper to create multiple property signals at once. -* **Signature:** `useProperties(map: Record): Record any>` +* **Signature:** `useProperties(map: { [K in keyof T]: string }): { [K in keyof T]: () => T[K] | undefined }` * **Returns:** An object where keys match the input map, and values are signal readers. ```typescript -const { x, y } = useProperties({ x: 0, y: 0 }); -// x() and y() are now signals +const { foo, bar } = useProperties<{ foo: number; bar: string }>({ + foo: 'prop.name.foo', + bar: 'prop.bar', +}); +// foo(): number | undefined +// bar(): string | undefined ``` --- diff --git a/packages/shadow-objects/docs/03-api/03-view-components.md b/packages/shadow-objects/docs/03-api/03-view-components.md index 877fa61..b925b00 100644 --- a/packages/shadow-objects/docs/03-api/03-view-components.md +++ b/packages/shadow-objects/docs/03-api/03-view-components.md @@ -118,16 +118,15 @@ class GameEntity { }); // Sync position to Shadow World - this.viewComponent.setProperties({ - x: this.x, - y: this.y - }); + this.viewComponent.setProperty('x', this.x); + this.viewComponent.setProperty('y', this.y); } update() { // Send updates every frame (or optimally, only on change) if (this.moved) { - this.viewComponent.setProperties({ x: this.x, y: this.y }); + this.viewComponent.setProperty('x', this.x); + this.viewComponent.setProperty('y', this.y); } } diff --git a/packages/shadow-objects/docs/04-patterns/best-practices.md b/packages/shadow-objects/docs/04-patterns/best-practices.md index a3f0143..7b93876 100644 --- a/packages/shadow-objects/docs/04-patterns/best-practices.md +++ b/packages/shadow-objects/docs/04-patterns/best-practices.md @@ -127,10 +127,10 @@ const myMeshResource = createResource( If you need multiple properties, avoid calling `useProperty` multiple times. Use `useProperties` to get a structured object of signals. ```typescript -const { x, y, visible } = useProperties({ +const { x, y, visible } = useProperties<{ x: number; y: number; visible: boolean }>({ x: "position-x", y: "position-y", - visible: "is-visible" + visible: "is-visible", }); ``` diff --git a/packages/shadow-objects/src/in-the-dark/Kernel.spec.ts b/packages/shadow-objects/src/in-the-dark/Kernel.spec.ts index 7dd9846..02ad0e3 100644 --- a/packages/shadow-objects/src/in-the-dark/Kernel.spec.ts +++ b/packages/shadow-objects/src/in-the-dark/Kernel.spec.ts @@ -338,6 +338,42 @@ describe('Kernel', () => { kernel.destroy(); }); + + it('should support typed property maps', () => { + const registry = new Registry(); + const kernel = new Kernel(registry); + + let capturedProps: + | { + foo: SignalReader; + bar: SignalReader; + } + | undefined; + + @ShadowObject({registry, token: 'testTypedUseProperties'}) + class TestTypedUseProperties { + constructor({useProperties}: ShadowObjectCreationAPI) { + const props = useProperties<{foo: number; bar: string}>({ + foo: 'propA', + bar: 'propB', + }); + capturedProps = props; + } + } + expect(TestTypedUseProperties).toBeDefined(); + + const uuid = generateUUID(); + kernel.createEntity(uuid, 'testTypedUseProperties', undefined, 0, [ + ['propA', 123], + ['propB', 'valueB'], + ]); + + expect(capturedProps).toBeDefined(); + expect(value(capturedProps!.foo)).toBe(123); + expect(value(capturedProps!.bar)).toBe('valueB'); + + kernel.destroy(); + }); }); describe('provideContext and useContext', () => { diff --git a/packages/shadow-objects/src/in-the-dark/Kernel.ts b/packages/shadow-objects/src/in-the-dark/Kernel.ts index f49b9a0..bd47b8d 100644 --- a/packages/shadow-objects/src/in-the-dark/Kernel.ts +++ b/packages/shadow-objects/src/in-the-dark/Kernel.ts @@ -360,12 +360,12 @@ export class Kernel { const contextProviders = new Map>(); const contextRootProviders = new Map>(); - const propertyReaders = new Map>(); + const propertyReaders = new Map>(); - const getUseProperty = ( + const getUseProperty = ( name: string, options?: SignalValueOptions | CompareFunc, - ): SignalReader => { + ): SignalReader> => { if (!usePropertyOptionsDeprecatedShown && options != null && typeof options === 'function') { console.warn( '[shadow-objects] Deprecation Warning: The "isEqual" option of "useProperty()" is now passed as {compare} argument. Please update your code accordingly.', @@ -375,11 +375,11 @@ export class Kernel { const opts = typeof options === 'function' ? {compare: options} : options; - let propReader = propertyReaders.get(name); + let propReader = propertyReaders.get(name) as SignalReader> | undefined; if (propReader === undefined) { - propReader = createSignal(undefined, opts).get; - propertyReaders.set(name, propReader); + propReader = createSignal>(undefined, opts).get; + propertyReaders.set(name, propReader as SignalReader); const con = link(entry.entity.getPropertyReader(name), propReader); unsubscribeSecondary.add(con.destroy.bind(con)); } @@ -523,8 +523,10 @@ export class Kernel { useProperty: getUseProperty, - useProperties(props: Record): Record> { - const result = {} as Record>; + useProperties = Record>( + props: {[K in keyof T]: string}, + ): {[K in keyof T]: SignalReader>} { + const result = {} as {[K in keyof T]: SignalReader>}; for (const key in props) { if (Object.hasOwn(props, key)) { result[key] = getUseProperty(props[key]); diff --git a/packages/shadow-objects/src/types.ts b/packages/shadow-objects/src/types.ts index 97c269a..f45ce8a 100644 --- a/packages/shadow-objects/src/types.ts +++ b/packages/shadow-objects/src/types.ts @@ -132,7 +132,9 @@ export interface ShadowObjectCreationAPI { useProperty(name: string, options?: SignalValueOptions | CompareFunc): SignalReader>; - useProperties(props: Record): Record>; + useProperties = Record>( + props: {[K in keyof T]: string}, + ): {[K in keyof T]: SignalReader>}; createResource(factory: () => T | undefined, cleanup?: (resource: NonNullable) => unknown): Signal>;