Skip to content

Commit 685201d

Browse files
Copilotspearwolf
andcommitted
Refine useProperties typing and docs
Co-authored-by: spearwolf <12805+spearwolf@users.noreply.github.com>
1 parent fdcdb00 commit 685201d

8 files changed

Lines changed: 69 additions & 23 deletions

File tree

packages/shadow-objects/CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
88
## unreleased
99

1010
- sharpen the `EntityApi` type definitions
11+
- improve `useProperties()` type inference with key-to-type maps
1112
- **Documentation:** Comprehensive update to the documentation structure and content.
1213
- Added dedicated documentation for Web Components (`<shae-worker>`, `<shae-ent>`, `<shae-prop>`) at `docs/03-api/04-web-components.md`.
1314
- Clarified the usage of Component Contexts, Namespacing (`ns` attribute), and decoupled placement of View Components.

packages/shadow-objects/docs/02-guides/02-creating-shadow-objects.md

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,13 +9,15 @@ The recommended way to define a Shadow Object is a simple function. This functio
99
```typescript
1010
import { ShadowObjectCreationAPI } from "@spearwolf/shadow-objects";
1111

12-
export function UserProfileLogic({
13-
useProperty,
14-
createEffect
12+
export function UserProfileLogic({
13+
useProperties,
14+
createEffect,
1515
}: ShadowObjectCreationAPI) {
1616

1717
// 1. Setup Phase: Define your reactive graph here
18-
const userId = useProperty('userId');
18+
const { userId } = useProperties<{userId: string}>({
19+
userId: 'userId',
20+
});
1921

2022
createEffect(() => {
2123
// 2. Runtime Phase: This runs whenever userId changes

packages/shadow-objects/docs/03-api/01-shadow-object-api.md

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -39,12 +39,16 @@ createEffect(() => {
3939

4040
A convenience helper to create multiple property signals at once.
4141

42-
* **Signature:** `useProperties(map: Record<string, any>): Record<string, () => any>`
42+
* **Signature:** `useProperties<T>(map: { [K in keyof T]: string }): { [K in keyof T]: () => T[K] | undefined }`
4343
* **Returns:** An object where keys match the input map, and values are signal readers.
4444

4545
```typescript
46-
const { x, y } = useProperties({ x: 0, y: 0 });
47-
// x() and y() are now signals
46+
const { foo, bar } = useProperties<{ foo: number; bar: string }>({
47+
foo: 'prop.name.foo',
48+
bar: 'prop.bar',
49+
});
50+
// foo(): number | undefined
51+
// bar(): string | undefined
4852
```
4953

5054
---

packages/shadow-objects/docs/03-api/03-view-components.md

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -118,16 +118,15 @@ class GameEntity {
118118
});
119119

120120
// Sync position to Shadow World
121-
this.viewComponent.setProperties({
122-
x: this.x,
123-
y: this.y
124-
});
121+
this.viewComponent.setProperty('x', this.x);
122+
this.viewComponent.setProperty('y', this.y);
125123
}
126124

127125
update() {
128126
// Send updates every frame (or optimally, only on change)
129127
if (this.moved) {
130-
this.viewComponent.setProperties({ x: this.x, y: this.y });
128+
this.viewComponent.setProperty('x', this.x);
129+
this.viewComponent.setProperty('y', this.y);
131130
}
132131
}
133132

packages/shadow-objects/docs/04-patterns/best-practices.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -127,10 +127,10 @@ const myMeshResource = createResource(
127127
If you need multiple properties, avoid calling `useProperty` multiple times. Use `useProperties` to get a structured object of signals.
128128

129129
```typescript
130-
const { x, y, visible } = useProperties({
130+
const { x, y, visible } = useProperties<{ x: number; y: number; visible: boolean }>({
131131
x: "position-x",
132132
y: "position-y",
133-
visible: "is-visible"
133+
visible: "is-visible",
134134
});
135135
```
136136

packages/shadow-objects/src/in-the-dark/Kernel.spec.ts

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -338,6 +338,42 @@ describe('Kernel', () => {
338338

339339
kernel.destroy();
340340
});
341+
342+
it('should support typed property maps', () => {
343+
const registry = new Registry();
344+
const kernel = new Kernel(registry);
345+
346+
let capturedProps:
347+
| {
348+
foo: SignalReader<number | undefined>;
349+
bar: SignalReader<string | undefined>;
350+
}
351+
| undefined;
352+
353+
@ShadowObject({registry, token: 'testTypedUseProperties'})
354+
class TestTypedUseProperties {
355+
constructor({useProperties}: ShadowObjectCreationAPI) {
356+
const props = useProperties<{foo: number; bar: string}>({
357+
foo: 'propA',
358+
bar: 'propB',
359+
});
360+
capturedProps = props;
361+
}
362+
}
363+
expect(TestTypedUseProperties).toBeDefined();
364+
365+
const uuid = generateUUID();
366+
kernel.createEntity(uuid, 'testTypedUseProperties', undefined, 0, [
367+
['propA', 123],
368+
['propB', 'valueB'],
369+
]);
370+
371+
expect(capturedProps).toBeDefined();
372+
expect(value(capturedProps!.foo)).toBe(123);
373+
expect(value(capturedProps!.bar)).toBe('valueB');
374+
375+
kernel.destroy();
376+
});
341377
});
342378

343379
describe('provideContext and useContext', () => {

packages/shadow-objects/src/in-the-dark/Kernel.ts

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -360,12 +360,12 @@ export class Kernel {
360360
const contextProviders = new Map<string | symbol, Signal<any>>();
361361
const contextRootProviders = new Map<string | symbol, Signal<any>>();
362362

363-
const propertyReaders = new Map<string, SignalReader<any>>();
363+
const propertyReaders = new Map<string, SignalReader<unknown>>();
364364

365-
const getUseProperty = <T = any>(
365+
const getUseProperty = <T = unknown>(
366366
name: string,
367367
options?: SignalValueOptions<T> | CompareFunc<T | undefined>,
368-
): SignalReader<T> => {
368+
): SignalReader<Maybe<T>> => {
369369
if (!usePropertyOptionsDeprecatedShown && options != null && typeof options === 'function') {
370370
console.warn(
371371
'[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 {
375375

376376
const opts = typeof options === 'function' ? {compare: options} : options;
377377

378-
let propReader = propertyReaders.get(name);
378+
let propReader = propertyReaders.get(name) as SignalReader<Maybe<T>> | undefined;
379379

380380
if (propReader === undefined) {
381-
propReader = createSignal<any>(undefined, opts).get;
382-
propertyReaders.set(name, propReader);
381+
propReader = createSignal<Maybe<T>>(undefined, opts).get;
382+
propertyReaders.set(name, propReader as SignalReader<unknown>);
383383
const con = link(entry.entity.getPropertyReader(name), propReader);
384384
unsubscribeSecondary.add(con.destroy.bind(con));
385385
}
@@ -523,8 +523,10 @@ export class Kernel {
523523

524524
useProperty: getUseProperty,
525525

526-
useProperties<K extends string>(props: Record<K, string>): Record<K, SignalReader<any>> {
527-
const result = {} as Record<K, SignalReader<any>>;
526+
useProperties<T extends Record<string, unknown> = Record<string, unknown>>(
527+
props: {[K in keyof T]: string},
528+
): {[K in keyof T]: SignalReader<Maybe<T[K]>>} {
529+
const result = {} as {[K in keyof T]: SignalReader<Maybe<T[K]>>};
528530
for (const key in props) {
529531
if (Object.hasOwn(props, key)) {
530532
result[key] = getUseProperty(props[key]);

packages/shadow-objects/src/types.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -132,7 +132,9 @@ export interface ShadowObjectCreationAPI {
132132

133133
useProperty<T = unknown>(name: string, options?: SignalValueOptions<T> | CompareFunc<T | undefined>): SignalReader<Maybe<T>>;
134134

135-
useProperties<K extends string>(props: Record<K, string>): Record<K, SignalReader<any>>;
135+
useProperties<T extends Record<string, unknown> = Record<string, unknown>>(
136+
props: {[K in keyof T]: string},
137+
): {[K in keyof T]: SignalReader<Maybe<T[K]>>};
136138

137139
createResource<T = unknown>(factory: () => T | undefined, cleanup?: (resource: NonNullable<T>) => unknown): Signal<Maybe<T>>;
138140

0 commit comments

Comments
 (0)