diff --git a/src/Schema.ts b/src/Schema.ts index 66029530..bce8b1bc 100644 --- a/src/Schema.ts +++ b/src/Schema.ts @@ -11,7 +11,7 @@ import { CollectionSchema } from './types/CollectionSchema'; import { SetSchema } from './types/SetSchema'; import { ChangeTree, Ref, ChangeOperation } from "./changes/ChangeTree"; -import { NonFunctionPropNames, ToJSON } from './types/HelperTypes'; +import { NonFunctionPropNames, ToJSON} from './types/HelperTypes'; import { ClientState } from './filters'; import { getType } from './types/typeRegistry'; import { ReferenceTracker } from './changes/ReferenceTracker'; @@ -894,7 +894,9 @@ export abstract class Schema { return cloned; } - toJSON () { + toJSON (): { + [Key in NonFunctionPropNames]: this[Key] extends Function ? never : ToJSON + } { const schema = this._definition.schema; const deprecated = this._definition.deprecated; @@ -906,7 +908,9 @@ export abstract class Schema { : this[`_${field}`]; } } - return obj as ToJSON; + return obj as { + [Key in NonFunctionPropNames]: this[Key] extends Function ? never : ToJSON + } } discardAllChanges() { diff --git a/src/types/ArraySchema.ts b/src/types/ArraySchema.ts index c62bffde..4cb32a8d 100644 --- a/src/types/ArraySchema.ts +++ b/src/types/ArraySchema.ts @@ -2,7 +2,7 @@ import { ChangeTree } from "../changes/ChangeTree"; import { OPERATION } from "../spec"; import { SchemaDecoderCallbacks, Schema } from "../Schema"; import { addCallback, removeChildRefs } from "./utils"; -import { DataChange } from ".."; +import { DataChange, ToJSON } from ".."; const DEFAULT_SORT = (a: any, b: any) => { const A = a.toString(); @@ -654,7 +654,7 @@ export class ArraySchema implements Array, SchemaDecoderCallbacks { return Array.from(this.$items.values()); } - toJSON() { + toJSON(): ToJSON[] { return this.toArray().map((value) => { return (typeof (value['toJSON']) === "function") ? value['toJSON']() diff --git a/src/types/CollectionSchema.ts b/src/types/CollectionSchema.ts index 1c55be62..4b8e0e4f 100644 --- a/src/types/CollectionSchema.ts +++ b/src/types/CollectionSchema.ts @@ -2,7 +2,7 @@ import { ChangeTree } from "../changes/ChangeTree"; import { OPERATION } from "../spec"; import { SchemaDecoderCallbacks } from "../Schema"; import { addCallback, removeChildRefs } from "./utils"; -import { DataChange } from ".."; +import { DataChange, ToJSON } from ".."; type K = number; // TODO: allow to specify K generic on MapSchema. @@ -157,8 +157,8 @@ export class CollectionSchema implements SchemaDecoderCallbacks { return Array.from(this.$items.values()); } - toJSON() { - const values: V[] = []; + toJSON(): ToJSON[] { + const values: ToJSON[] = []; this.forEach((value, key) => { values.push( diff --git a/src/types/HelperTypes.ts b/src/types/HelperTypes.ts index 3c3908ef..a2c39d17 100644 --- a/src/types/HelperTypes.ts +++ b/src/types/HelperTypes.ts @@ -1,6 +1,3 @@ -import { ArraySchema } from "./ArraySchema"; -import { MapSchema } from "./MapSchema"; - export type NonFunctionProps = Omit; @@ -9,12 +6,6 @@ export type NonFunctionPropNames = { [K in keyof T]: T[K] extends Function ? never : K }[keyof T]; -export type ToJSON = NonFunctionProps<{ - [K in keyof T]: T[K] extends MapSchema - ? Record - : T[K] extends Map - ? Record - : T[K] extends ArraySchema - ? U[] - : T[K] -}>; \ No newline at end of file +export type ToJSON = T extends { + toJSON(): unknown +} ? ReturnType : T \ No newline at end of file diff --git a/src/types/MapSchema.ts b/src/types/MapSchema.ts index a4baec1b..2a60b2f5 100644 --- a/src/types/MapSchema.ts +++ b/src/types/MapSchema.ts @@ -1,6 +1,6 @@ import { SchemaDecoderCallbacks } from "../Schema"; import { addCallback, removeChildRefs } from "./utils"; -import { DataChange } from ".."; +import { DataChange, ToJSON } from ".."; import { ChangeTree } from "../changes/ChangeTree"; import { OPERATION } from "../spec"; @@ -243,7 +243,7 @@ export class MapSchema implements Map, S this.$indexes.delete(index); } - toJSON() { + toJSON(): { [key in K]: ToJSON } { const map: any = {}; this.forEach((value, key) => { diff --git a/src/types/SetSchema.ts b/src/types/SetSchema.ts index c3f3569b..2334ec85 100644 --- a/src/types/SetSchema.ts +++ b/src/types/SetSchema.ts @@ -2,7 +2,7 @@ import { ChangeTree } from "../changes/ChangeTree"; import { OPERATION } from "../spec"; import { SchemaDecoderCallbacks } from "../Schema"; import { addCallback, removeChildRefs } from "./utils"; -import { DataChange } from ".."; +import { DataChange, ToJSON } from ".."; export class SetSchema implements SchemaDecoderCallbacks { protected $changes: ChangeTree = new ChangeTree(this); @@ -166,8 +166,8 @@ export class SetSchema implements SchemaDecoderCallbacks { return Array.from(this.$items.values()); } - toJSON() { - const values: V[] = []; + toJSON(): ToJSON[] { + const values: ToJSON[] = []; this.forEach((value, key) => { values.push( diff --git a/test/helpers/Equals.ts b/test/helpers/Equals.ts new file mode 100644 index 00000000..88ba4150 --- /dev/null +++ b/test/helpers/Equals.ts @@ -0,0 +1,7 @@ +// Used to check if two types are equal. +// If they are equal, the expression evaluates to true, otherwise false +export type Equals = T1 extends T2 + ? T2 extends T1 + ? true + : false + : false \ No newline at end of file diff --git a/test/src/HelperTypes.test.ts b/test/src/HelperTypes.test.ts new file mode 100644 index 00000000..3ca2a5d5 --- /dev/null +++ b/test/src/HelperTypes.test.ts @@ -0,0 +1,236 @@ +import { ArraySchema, CollectionSchema, MapSchema, Schema, SetSchema, ToJSON, type } from "../../src"; +import { Equals } from "../helpers/Equals"; + +// Reused across multiple tests +export class VecSchema extends Schema { + @type('number') x: number + @type('number') y: number +} + +describe("ToJSON type tests", () => { + it("Omits methods", () => { + class C extends Schema { + @type('number') time: number + rewind(arg: number){} + } + const _t1: Equals, { + time: number + }> = true + }) + + it("Does not transform primitive types", () => { + // Primitive types have methods, and these should not be omitted + const _tString: Equals, string> = true + const _tNumber: Equals, number> = true + const _tBoolean: Equals, boolean> = true + const _tBigInt: Equals, bigint> = true + const _tSymbol: Equals, symbol> = true + const _tUndefined: Equals, undefined> = true + const _tNull: Equals, null> = true + }) + + it("Does not transform non-schema types", () => { + class C extends Schema { + time: number + pos: { x: number, y: number } + } + const _t1: Equals, { + time: number + pos: { x: number, y: number } + }> = true + const _t2: ToJSON = { + time: 1, + pos: { x: 1, y: 2 } + } + }) + + it("Primitive type on root", () => { + class C extends Schema { + @type('number') time: number + @type('string') name: string + } + const _t1: Equals, { + time: number + name: string + }> = true + const _t2: ToJSON = { + time: 1, + name: "name" + } + }) + + it("Schema type on root", () => { + class C extends Schema { + @type(VecSchema) ballPos: VecSchema + } + const _t1: Equals, { + ballPos: { + x: number + y: number + } + }> = true + const _t2: ToJSON = { + ballPos: { + x: 1, + y: 2 + } + } + }) + + describe("MapSchema", () => { + it("allows MapSchema on root", () => { + class C extends Schema { + @type({map: VecSchema}) positions: MapSchema + } + + const _t1: Equals, { + positions: Record + }> = true + const _t2: ToJSON = { + positions: { + a: { + x: 1, + y: 2 + } + } + } + }) + + it("allows recursive schemas", () => { + class C extends Schema { + @type({ map: C }) mapToSelf: MapSchema + } + type T1 = { + mapToSelf: Record + } + const _t1: Equals, T1> = true + const _t2: ToJSON = { + mapToSelf: { + a: { + mapToSelf: { + } + } + } + } + }) + }) + + describe("ArraySchema", () => { + it("allows ArraySchema on root", () => { + class C extends Schema { + @type({array: VecSchema}) positions: ArraySchema + } + const _t1: Equals, { + positions: Array<{ + x: number + y: number + }> + }> = true + const _t2: ToJSON = { + positions: [{ + x: 1, + y: 2 + }] + } + }) + + it("allows recursive schemas", () => { + class C extends Schema { + @type({ array: C }) arrayOfSelf: ArraySchema + } + type T1 = { + arrayOfSelf: Array + } + const _t1: Equals, T1> = true + const _t2: ToJSON = { + arrayOfSelf: [{ + arrayOfSelf: [] + }, { + arrayOfSelf: [{ + arrayOfSelf: [] + }] + }] + } + }) + }) + + describe("SetSchema", () => { + it("allows SetSchema on root", () => { + class C extends Schema { + @type({set: VecSchema}) positions: SetSchema + } + const _t1: Equals, { + positions: Array<{ + x: number + y: number + }> + }> = true + const _t2: ToJSON = { + positions: [{ + x: 1, + y: 2 + }] + } + }) + it("allows recursive schemas", () => { + class C extends Schema { + @type({ set: C }) setOfSelf: SetSchema + } + type T1 = { + setOfSelf: Array + } + const _t1: Equals, T1> = true + const _t2: ToJSON = { + setOfSelf: [{ + setOfSelf: [] + }, { + setOfSelf: [{ + setOfSelf: [] + }] + }] + } + }) + }) + + describe("CollectionSchema", () => { + it("allows CollectionSchema on root", () => { + class C extends Schema { + @type({collection: VecSchema}) positions: CollectionSchema + } + const _t1: Equals, { + positions: Array<{ + x: number + y: number + }> + }> = true + const _t2: ToJSON = { + positions: [{ + x: 1, + y: 2 + }] + } + }) + + it("allows recursive schemas", () => { + class C extends Schema { + @type({ collection: C }) collectionOfSelf: CollectionSchema + } + type T1 = { + collectionOfSelf: Array + } + const _t1: Equals, T1> = true + const _t2: ToJSON = { + collectionOfSelf: [{ + collectionOfSelf: [] + }, { + collectionOfSelf: [{ + collectionOfSelf: [] + }] + }] + } + }) + }) +}) \ No newline at end of file diff --git a/test/TypeScriptTypes.test.ts b/test/src/TypeScriptTypes.test.ts similarity index 93% rename from test/TypeScriptTypes.test.ts rename to test/src/TypeScriptTypes.test.ts index 15132b12..e4cea119 100644 --- a/test/TypeScriptTypes.test.ts +++ b/test/src/TypeScriptTypes.test.ts @@ -1,6 +1,6 @@ import * as assert from "assert"; -import { State, Player, DeepState, DeepMap, DeepChild, Position, DeepEntity } from "./Schema"; -import { Schema, ArraySchema, MapSchema, type } from "../src"; +import { State, Player, DeepState, DeepMap, DeepChild, Position, DeepEntity } from "../Schema"; +import { Schema, ArraySchema, MapSchema, type } from "../../src"; describe("TypeScript Types", () => { it("strict null/undefined checks", () => {