import { $resyncPrune } from "./symbols.js"; import type { Definition, DefinitionType, PrimitiveType, RawPrimitiveType } from "../annotations.js"; import type { Schema } from "../Schema.js"; import type { ArraySchema } from "./custom/ArraySchema.js"; import type { CollectionSchema } from "./custom/CollectionSchema.js"; import type { MapSchema } from "./custom/MapSchema.js"; import type { SetSchema } from "./custom/SetSchema.js"; import type { StreamSchema } from "./custom/StreamSchema.js"; import type { FieldBuilder } from "./builder.js"; export type Constructor = new (...args: any[]) => T; // Helper to convert primitive type literals to actual runtime types type PrimitiveStringToType = T extends "string" ? string : T extends "number" | "int8" | "uint8" | "int16" | "uint16" | "int32" | "uint32" | "int64" | "uint64" | "float32" | "float64" ? number : T extends "bigint64" | "biguint64" ? bigint : T extends "boolean" ? boolean : T; /** * What the decoder callbacks accept as "a collection": the public shape, which * a plain array satisfies too — `@type([X]) items: X[]` is a common way to * declare a field. {@link Collection} is the runtime contract on top of it. */ export interface CollectionLike { [Symbol.iterator](): IterableIterator; forEach(callback: Function): void; entries(): IterableIterator<[K, V]>; } export interface Collection extends CollectionLike { /** See {@link $resyncPrune} — every collection kind must declare its resync-sweep semantics. */ [$resyncPrune]( visited: Set, prune: (value: V, identity: number | string) => void, keep: (value: V) => void, ): void; } export type InferValueType = // FieldBuilder unwraps to V (used by the zod-style schema() API) T extends FieldBuilder ? V : T extends "string" ? string : T extends "number" ? number : T extends "int8" ? number : T extends "uint8" ? number : T extends "int16" ? number : T extends "uint16" ? number : T extends "int32" ? number : T extends "uint32" ? number : T extends "int64" ? number : T extends "uint64" ? number : T extends "float32" ? number : T extends "float64" ? number : T extends "bigint64" ? bigint : T extends "biguint64" ? bigint : T extends "boolean" ? boolean // Handle { type: ... } patterns : T extends { type: infer ChildType extends PrimitiveType } ? InferValueType : T extends { type: infer ChildType extends Constructor } ? InstanceType : T extends { type: Array } ? (ChildType extends Record ? ChildType[keyof ChildType][] : ChildType[]) // TS ENUM : T extends { type: { map: infer ChildType } } ? (ChildType extends Record ? MapSchema : MapSchema) // TS ENUM : T extends { type: { set: infer ChildType } } ? (ChildType extends Record ? SetSchema : SetSchema) // TS ENUM : T extends { type: { collection: infer ChildType } } ? (ChildType extends Record ? CollectionSchema : CollectionSchema) // TS ENUM : T extends { type: { stream: infer ChildType extends Constructor } } ? StreamSchema> : T extends { type: { stream: infer ChildType } } ? StreamSchema : T extends { type: infer ChildType } ? (ChildType extends Record ? ChildType[keyof ChildType] : ChildType) // TS ENUM // Handle direct array patterns : T extends Array ? ArraySchema> : T extends Array ? (ChildType extends Record ? ArraySchema : ArraySchema>) // TS ENUM // Handle collection object patterns : T extends { array: infer ChildType extends Constructor } ? ArraySchema> : T extends { array: infer ChildType } ? (ChildType extends Record ? ArraySchema : ArraySchema>) // TS ENUM : T extends { map: infer ChildType extends Constructor } ? MapSchema> : T extends { map: infer ChildType } ? (ChildType extends Record ? MapSchema : MapSchema>) // TS ENUM : T extends { set: infer ChildType extends Constructor } ? SetSchema> : T extends { set: infer ChildType extends RawPrimitiveType } ? SetSchema> // primitive types : T extends { set: infer ChildType } ? (ChildType extends Record ? SetSchema : SetSchema) // TS ENUM : T extends { collection: infer ChildType extends Constructor } ? CollectionSchema> : T extends { collection: infer ChildType extends RawPrimitiveType } ? CollectionSchema> // primitive types : T extends { collection: infer ChildType } ? (ChildType extends Record ? CollectionSchema : CollectionSchema) // TS ENUM : T extends { stream: infer ChildType extends Constructor } ? StreamSchema> : T extends { stream: infer ChildType } ? StreamSchema // Handle direct types : T extends Constructor ? InstanceType : T extends Record ? T[keyof T] // TS ENUM : T extends PrimitiveType ? T : never; /** * Codecs that can carry a `T` — {@link InferValueType} run backwards, derived * from it so the two can't drift. Constrains the element refinement in * `t.array("uint8")`; `never` (an uncallable overload) when no codec * decodes into `T`. * * `[T] extends [...]` is deliberate: a distributive check would let a mixed * union like `string | number` match on either half. */ export type CodecFor = { [K in RawPrimitiveType]: [T] extends [InferValueType] ? K : never }[RawPrimitiveType]; // Keys whose builder carries the `.optional()` brand. Reads the brand rather // than `undefined extends V`: the latter is true for EVERY V when the consumer // compiles with `strictNullChecks: false`, flipping all fields optional. type IsOptionalBuilderKey = T[K] extends FieldBuilder ? O : false; // THE RULE for every mapped type below that projects a user's declared fields: // split in the `as` clause, never over a precomputed key union. Only a // homomorphic mapping carries each key back to its declaration, and that link // is what go-to-definition and rename resolve through — losing it leaves rename // silently touching just the cursor (colyseus/colyseus#958). The `-readonly` // and `-?` that follow drop the modifiers such a mapping inherits from `T`. export type InferSchemaInstanceType = { -readonly [K in keyof T as IsOptionalBuilderKey extends true ? never : K]-?: T[K] extends FieldBuilder ? InferValueType : T[K] extends (...args: any[]) => any ? (T[K] extends new (...args: any[]) => any ? InferValueType : T[K]) : InferValueType } & { -readonly [K in keyof T as IsOptionalBuilderKey extends true ? K : never]?: T[K] extends FieldBuilder ? V : never } & Schema; // Per-key filter, never `Omit`/`Exclude` over the union of method names: with // one field typed by a bare type parameter that union defers EVERY key, and a // mapped type with no resolvable keys has no members to relate — which is what // stopped `SpecialNode` from satisfying `extends NodeBase`. // `keyof Schema` is dropped so `restore({ ... })` takes a plain literal. type DataKey = K extends keyof Schema ? never : T[K] extends Function ? never : K; export type NonFunctionPropNames = { [K in keyof T]-?: DataKey }[keyof T]; export type NonFunctionNonPrimitivePropNames = { [K in keyof T]-?: [DataKey] extends [never] ? never : T[K] extends number | string | boolean ? never : K }[keyof T]; // Helper to recursively convert Schema instances to their JSON representation type ToJSONValue = U extends Schema ? ToJSON : PrimitiveStringToType; type ToJSONField = X extends MapSchema ? Record> : X extends Map ? Record> : X extends ArraySchema ? ToJSONValue[] : X extends SetSchema ? ToJSONValue[] : X extends CollectionSchema ? ToJSONValue[] : X extends Schema ? ToJSON : X; // Runtime `toJSON()` omits `undefined` values, so those keys surface as `?:`. // Under `strictNullChecks: false` (`undefined extends {}` detects it) // `undefined extends T[K]` is true for every key, so only the `?` modifier // can signal optionality there. type IsOptionalKey = undefined extends {} ? ({} extends Pick ? true : false) : (undefined extends T[K] ? true : false); // Beyond THE RULE, the `as` clause is load-bearing here for a second reason: it // runs before the value type, so `ToJSONField` never reaches the machinery, // where it would recurse through `restore(json: ToJSON)` past TypeScript // 7's instantiation limit. Probing `IsOptionalKey` first is deliberate too — // `DataKey` then runs in the taken branch only, once per key not once per half. export type ToJSON = & { -readonly [K in keyof T as IsOptionalKey extends true ? never : DataKey]-?: ToJSONField } & { -readonly [K in keyof T as IsOptionalKey extends true ? DataKey : never]?: ToJSONField> }; /** * The plain DATA shape of a Schema instance type `T`: its synchronized fields * with all `Schema` machinery stripped (`assign`, `clone`, `toJSON`, the * change-tracking state, the internal symbol keys, …), so a plain object literal * satisfies it. Field types — including narrowed primitives like * `t.int8<-1 | 0 | 1>()` — are preserved exactly. * * Use it to type code that operates on schema-shaped *plain objects* rather than * decoded instances: deterministic simulation / physics steps, synthesized or * buffered input commands, plain DTOs, etc. * * ```ts * function applyInput(state: Player, cmd: Data) { … } * applyInput(player, { moveX: 1, jump: false, dt }); // plain literal — OK * ``` * * Unlike {@link ToJSON} (a recursive *serialization* shape), this is a flat * structural projection: nested Schema / collection fields keep their instance * types. */ export type Data = Omit; // Helper type to check if T is exactly 'never' (meaning no InitProps was provided) export type IsNever = [T] extends [never] ? true : false; /** * Type helper for .assign() method - allows assigning values in a flexible way * - Primitives can be assigned directly * - Schema instances can be assigned from plain objects or Schema instances * - Collections can be assigned from their JSON representations * * Keys filter through `DataKey` in the `as` clause — see THE RULE above. */ export type AssignableProps = { -readonly [K in keyof T as DataKey]?: AssignableValue }; /** * Value-level assignment shape shared by `AssignableProps` and * `BuilderInitProps`. Captures the "you can pass the real instance, or the * plain-object / array shape" pattern. */ export type AssignableValue = V extends MapSchema ? MapSchema | Record) : U> : V extends ArraySchema ? ArraySchema | (U extends Schema ? (U | AssignableProps)[] : U[]) : V extends SetSchema ? SetSchema | Set | (U extends Schema ? (U | AssignableProps)[] : U[]) : V extends CollectionSchema ? CollectionSchema | (U extends Schema ? (U | AssignableProps)[] : U[]) : V extends Schema ? V | AssignableProps : V; // --------------------------------------------------------------------------- // BuilderInitProps — init-props shape derived from a schema() fields map. // Unlike AssignableProps (fully partial, for `.assign()` updates), this type // enforces required vs optional based on per-field `HasDefault` + `undefined`. // --------------------------------------------------------------------------- // Compile-time analogue of schema()'s Schema-ref auto-default rule: // if the ref has no `initialize`, or a zero-arg `initialize`, schema() // auto-instantiates it — so the field is omittable at construction. export type RefHasDefault = C extends { prototype: { initialize(...args: infer P): any } } ? (P extends readonly [] ? true : false) : true; // Resolve a fields-map entry to its runtime value type. type FieldValue = F extends FieldBuilder ? V : F extends new (...args: any[]) => infer I ? (I extends Schema ? I : never) : never; // Classify each key of a fields map as "required" / "optional" / "none" // (methods). Both `HasDefault = true` and the explicit `.optional()` brand // `IsOptional = true` mark the field omittable at construction. Reading the // brands (never `undefined extends V`) keeps this correct for consumers on // `strictNullChecks: false`, where `undefined extends V` is true for every V. type KeyClass = T[K] extends FieldBuilder ? (D extends true ? "optional" : O extends true ? "optional" : "required") : T[K] extends new (...args: any[]) => Schema ? (RefHasDefault extends true ? "optional" : "required") : "none"; /** * Constructor/init-props type for a schema() fields map. Required fields * (primitives without `.default()` or `.optional()`, and Schema refs with * non-zero-arg `initialize()`) are `:`; everything else is `?:`. * * Split by `KeyClass` in the `as` clause — see THE RULE above. `-?` matters * under `strictNullChecks: false`: an optional key in the fields map still * classifies as "required" there, and would otherwise inherit the `?`. */ export type BuilderInitProps = & { -readonly [K in keyof T as KeyClass extends "required" ? K : never]-?: AssignableValue> } & { -readonly [K in keyof T as KeyClass extends "optional" ? K : never]?: AssignableValue, undefined>> };