refactor: improve type safety in World and tests
Replace `any` types with specific interfaces like `WorldEvent`, `QueryUpdate`, and `Entity` to strengthen type checking. This includes refining the deserialization logic in `World.fromSnapshot` to use properly typed component definitions.
This commit is contained in:
parent
9953c7c556
commit
05674a349f
11
src/world.ts
11
src/world.ts
|
|
@ -7,6 +7,7 @@ import { ObservableLayer } from "./observable/observe";
|
||||||
import type { QueryUpdate, RelationshipUpdate } from "./observable/events";
|
import type { QueryUpdate, RelationshipUpdate } from "./observable/events";
|
||||||
import type { RelationshipDef } from "./relationship";
|
import type { RelationshipDef } from "./relationship";
|
||||||
import { Observable } from "rxjs";
|
import { Observable } from "rxjs";
|
||||||
|
import type { WorldEvent } from "./observable/events";
|
||||||
import type { WorldSnapshot } from "./serialization";
|
import type { WorldSnapshot } from "./serialization";
|
||||||
|
|
||||||
// ── World ─────────────────────────────────────────────
|
// ── World ─────────────────────────────────────────────
|
||||||
|
|
@ -40,7 +41,7 @@ export class World {
|
||||||
private _observable = new ObservableLayer();
|
private _observable = new ObservableLayer();
|
||||||
|
|
||||||
/** Global event stream. */
|
/** Global event stream. */
|
||||||
get events$(): Observable<any> {
|
get events$(): Observable<WorldEvent> {
|
||||||
return this._observable.events$.asObservable();
|
return this._observable.events$.asObservable();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -435,7 +436,10 @@ export class World {
|
||||||
): World {
|
): World {
|
||||||
const world = new World();
|
const world = new World();
|
||||||
|
|
||||||
const compByName = new Map(components.map((c) => [c.name, c]));
|
const compByName = new Map(components.map((c) => [c.name, c])) as Map<
|
||||||
|
string,
|
||||||
|
ComponentDef<Record<string, unknown>>
|
||||||
|
>;
|
||||||
const relByName = new Map((relationships ?? []).map((r) => [r.name, r]));
|
const relByName = new Map((relationships ?? []).map((r) => [r.name, r]));
|
||||||
|
|
||||||
// Map string ids → real Entity handles
|
// Map string ids → real Entity handles
|
||||||
|
|
@ -453,7 +457,8 @@ export class World {
|
||||||
`Pass it in the components array.`,
|
`Pass it in the components array.`,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
world.add(entity, def, value as any);
|
// Unknown at deserialization boundary; shape matches ComponentDef.defaults
|
||||||
|
world.add(entity, def, value as Partial<Record<string, unknown>>);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,7 @@ import {
|
||||||
type WorldSnapshot,
|
type WorldSnapshot,
|
||||||
query,
|
query,
|
||||||
type RelationshipUpdate,
|
type RelationshipUpdate,
|
||||||
|
type WorldEvent,
|
||||||
} from "../src/index";
|
} from "../src/index";
|
||||||
|
|
||||||
// ── Definitions ─────────────────────────────────────
|
// ── Definitions ─────────────────────────────────────
|
||||||
|
|
@ -269,8 +270,9 @@ describe("Serialization — complex state", () => {
|
||||||
const playerId = snap.relationships.ownedBy[bulletId];
|
const playerId = snap.relationships.ownedBy[bulletId];
|
||||||
|
|
||||||
// Player should have a name
|
// Player should have a name
|
||||||
expect(snap.entities[playerId]).toHaveProperty("name");
|
const playerComps = snap.entities[playerId];
|
||||||
expect((snap.entities[playerId].name as any).value).toBe("Hero");
|
expect(playerComps).toHaveProperty("name");
|
||||||
|
expect((playerComps.name as { value: string }).value).toBe("Hero");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -403,8 +405,8 @@ describe("Serialization — observables after load", () => {
|
||||||
w.spawn();
|
w.spawn();
|
||||||
|
|
||||||
const loaded = roundTrip(w);
|
const loaded = roundTrip(w);
|
||||||
const events: any[] = [];
|
const events: WorldEvent[] = [];
|
||||||
loaded.events$.subscribe((e: any) => events.push(e));
|
loaded.events$.subscribe((e) => events.push(e));
|
||||||
|
|
||||||
const e = loaded.spawn();
|
const e = loaded.spawn();
|
||||||
expect(events).toHaveLength(1);
|
expect(events).toHaveLength(1);
|
||||||
|
|
@ -417,8 +419,8 @@ describe("Serialization — observables after load", () => {
|
||||||
w.add(e, Position, { x: 1, y: 2 });
|
w.add(e, Position, { x: 1, y: 2 });
|
||||||
|
|
||||||
const loaded = roundTrip(w);
|
const loaded = roundTrip(w);
|
||||||
const updates: any[] = [];
|
const updates: QueryUpdate[] = [];
|
||||||
loaded.observe(query(Position)).subscribe((u: any) => {
|
loaded.observe(query(Position)).subscribe((u) => {
|
||||||
if (u.added.length || u.removed.length || u.changed.length) {
|
if (u.added.length || u.removed.length || u.changed.length) {
|
||||||
updates.push(u);
|
updates.push(u);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@ import {
|
||||||
query,
|
query,
|
||||||
type QueryUpdate,
|
type QueryUpdate,
|
||||||
type WorldEvent,
|
type WorldEvent,
|
||||||
|
type Entity,
|
||||||
} from "../src/index";
|
} from "../src/index";
|
||||||
|
|
||||||
// ── Components ──────────────────────────────────────
|
// ── Components ──────────────────────────────────────
|
||||||
|
|
@ -84,11 +85,11 @@ describe("Entity lifecycle", () => {
|
||||||
// ── Components ──────────────────────────────────────
|
// ── Components ──────────────────────────────────────
|
||||||
describe("Components", () => {
|
describe("Components", () => {
|
||||||
let world: World;
|
let world: World;
|
||||||
let entity = 0 as any;
|
let entity: Entity;
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
world = new World();
|
world = new World();
|
||||||
entity = world.spawn();
|
entity = world.spawn() as Entity;
|
||||||
});
|
});
|
||||||
|
|
||||||
it("add returns defaults", () => {
|
it("add returns defaults", () => {
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue