From b0f3387368ef13422e2aa3b35b28b138e994c09b Mon Sep 17 00:00:00 2001 From: FTWinston Date: Sat, 3 Oct 2026 00:56:30 +0100 Subject: [PATCH 1/2] MapSchema snapshots are now partial --- src/schema/createSnapshot.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/schema/createSnapshot.ts b/src/schema/createSnapshot.ts index 5265e84..ad853b2 100644 --- a/src/schema/createSnapshot.ts +++ b/src/schema/createSnapshot.ts @@ -100,7 +100,7 @@ export type Snapshot = DeepReadonly< : T extends IArray ? Snapshot[] : T extends IMap - ? Record> + ? Partial>> : T extends Primitive ? T : T extends object @@ -172,11 +172,11 @@ export function getRefId(node: object): number { */ function createSnapshotForMapSchema( node: MapSchema, - previousResult: Record | undefined, + previousResult: Partial> | undefined, ctx: SnapshotContext, isDerived: boolean -): Record { - const snapshotted: Record = {}; +): Partial> { + const snapshotted: Partial> = {}; let hasChanged = previousResult === undefined; for (const [key, value] of node) { From f1d2510be1fd2c35090eaa026ee37135f9313bd6 Mon Sep 17 00:00:00 2001 From: Endel Dreyer Date: Fri, 2 Oct 2026 23:24:48 -0300 Subject: [PATCH 2/2] Snapshot: make map entries optional only for finite key unions MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Partial on every map typed Object.values() entries as possibly undefined, and MapSchema snapshots skipped the IMap branch entirely. Wide keys keep the index signature; only literal unions, where Record demanded every key, become optional — for MapSchema and IMap alike. Assisted-by: Claude Opus 5.5 --- src/schema/createSnapshot.ts | 26 +++++++++++++++++++------- src/tests/snapshotType.test.ts | 11 +++++++++++ 2 files changed, 30 insertions(+), 7 deletions(-) diff --git a/src/schema/createSnapshot.ts b/src/schema/createSnapshot.ts index ad853b2..3233223 100644 --- a/src/schema/createSnapshot.ts +++ b/src/schema/createSnapshot.ts @@ -76,11 +76,23 @@ type Primitive = string | number | boolean | bigint | symbol | null | undefined; /** Anything usable as a Map key. (MapSchema keys are always strings.) */ type MapKey = string | number | symbol; +/** + * Plain-object form of a map snapshot. Wide keys (`string`, `` `p_${string}` ``) + * stay an index signature, so `Object.values()` yields defined entries. A finite + * key union (`"red" | "blue"`) becomes optional properties, since a map need not + * hold every key. The test: an empty object only satisfies a `Record` with no + * required keys. + */ +type MapSnapshot = Record extends Record + ? Record> + : Partial>>; + /** * Transforms a Colyseus Schema type into an immutable, plain JavaScript type. * * - `ArraySchema` (or `Array` / `ReadonlyArray`) becomes `readonly T[]` - * - `MapSchema` (or `Map` / `ReadonlyMap`) becomes `Readonly>` + * - `MapSchema` (or `Map` / `ReadonlyMap`) becomes `Readonly>`, + * with every key optional when `K` is a finite union (see `MapSnapshot`) * - `Schema` subclasses (and plain objects) become plain objects with only data properties * - Primitives remain unchanged * @@ -95,12 +107,12 @@ type MapKey = string | number | symbol; export type Snapshot = DeepReadonly< T extends ArraySchema ? Snapshot[] - : T extends MapSchema - ? Record> + : T extends MapSchema + ? MapSnapshot : T extends IArray ? Snapshot[] : T extends IMap - ? Partial>> + ? MapSnapshot : T extends Primitive ? T : T extends object @@ -172,11 +184,11 @@ export function getRefId(node: object): number { */ function createSnapshotForMapSchema( node: MapSchema, - previousResult: Partial> | undefined, + previousResult: Record | undefined, ctx: SnapshotContext, isDerived: boolean -): Partial> { - const snapshotted: Partial> = {}; +): Record { + const snapshotted: Record = {}; let hasChanged = previousResult === undefined; for (const [key, value] of node) { diff --git a/src/tests/snapshotType.test.ts b/src/tests/snapshotType.test.ts index 837961c..792bd70 100644 --- a/src/tests/snapshotType.test.ts +++ b/src/tests/snapshotType.test.ts @@ -44,6 +44,17 @@ expectMutual["weapons"], { readonly [k: string]: W }>(true expectMutual["loadout"], ReadonlyArray>(true); expectMutual["weapons"], { readonly [k: string]: W }>(true); +// Finite key unions — a map need not hold every key, so entries are optional. +type Slot = "primary" | "secondary"; +type SlotW = { readonly primary?: W; readonly secondary?: W }; +expectMutual>, SlotW>(true); +expectMutual>, SlotW>(true); + +// Wide keys stay an index signature, so iterated entries are never `undefined`. +expectMutual>, { readonly [k: `p_${string}`]: W }>(true); +const values: W[] = Object.values({} as Snapshot["weapons"]); +void values; + // Primitives pass through. expectMutual, number>(true); expectMutual, string>(true);