@colyseus/schema 5.0.0 → 5.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -66,8 +66,14 @@ export type InferValueType<T> = T extends FieldBuilder<infer V> ? V : T extends
66
66
  } ? StreamSchema<InstanceType<ChildType>> : T extends {
67
67
  stream: infer ChildType;
68
68
  } ? StreamSchema<ChildType> : T extends Constructor ? InstanceType<T> : T extends Record<string | number, string | number> ? T[keyof T] : T extends PrimitiveType ? T : never;
69
+ type OptionalBuilderKeys<T> = {
70
+ [K in keyof T]: T[K] extends FieldBuilder<infer V> ? (undefined extends V ? K : never) : never;
71
+ }[keyof T];
72
+ type RequiredBuilderKeys<T> = Exclude<keyof T, OptionalBuilderKeys<T>>;
69
73
  export type InferSchemaInstanceType<T> = {
70
- [K in keyof T]: T[K] extends FieldBuilder<any> ? InferValueType<T[K]> : T[K] extends (...args: any[]) => any ? (T[K] extends new (...args: any[]) => any ? InferValueType<T[K]> : T[K]) : InferValueType<T[K]>;
74
+ [K in RequiredBuilderKeys<T>]: T[K] extends FieldBuilder<any> ? InferValueType<T[K]> : T[K] extends (...args: any[]) => any ? (T[K] extends new (...args: any[]) => any ? InferValueType<T[K]> : T[K]) : InferValueType<T[K]>;
75
+ } & {
76
+ [K in OptionalBuilderKeys<T>]?: T[K] extends FieldBuilder<infer V> ? V : never;
71
77
  } & Schema;
72
78
  export type NonFunctionProps<T> = Omit<T, {
73
79
  [K in keyof T]: T[K] extends Function ? K : never;
@@ -79,8 +85,17 @@ export type NonFunctionNonPrimitivePropNames<T> = {
79
85
  [K in keyof T]: T[K] extends Function ? never : T[K] extends number | string | boolean ? never : K;
80
86
  }[keyof T];
81
87
  type ToJSONValue<U> = U extends Schema ? ToJSON<U> : PrimitiveStringToType<U>;
88
+ type ToJSONField<X> = X extends MapSchema<infer U> ? Record<string, ToJSONValue<U>> : X extends Map<string, infer U> ? Record<string, ToJSONValue<U>> : X extends ArraySchema<infer U> ? ToJSONValue<U>[] : X extends SetSchema<infer U> ? ToJSONValue<U>[] : X extends CollectionSchema<infer U> ? ToJSONValue<U>[] : X extends Schema ? ToJSON<X> : X;
89
+ type ToJSONRequiredKeys<T> = {
90
+ [K in keyof T]-?: undefined extends T[K] ? never : K;
91
+ }[keyof T];
92
+ type ToJSONOptionalKeys<T> = {
93
+ [K in keyof T]-?: undefined extends T[K] ? K : never;
94
+ }[keyof T];
82
95
  export type ToJSON<T> = NonFunctionProps<{
83
- [K in keyof T]: T[K] extends MapSchema<infer U> ? Record<string, ToJSONValue<U>> : T[K] extends Map<string, infer U> ? Record<string, ToJSONValue<U>> : T[K] extends ArraySchema<infer U> ? ToJSONValue<U>[] : T[K] extends SetSchema<infer U> ? ToJSONValue<U>[] : T[K] extends CollectionSchema<infer U> ? ToJSONValue<U>[] : T[K] extends Schema ? ToJSON<T[K]> : T[K];
96
+ [K in ToJSONRequiredKeys<T>]: ToJSONField<T[K]>;
97
+ } & {
98
+ [K in ToJSONOptionalKeys<T>]?: ToJSONField<Exclude<T[K], undefined>>;
84
99
  }>;
85
100
  export type IsNever<T> = [T] extends [never] ? true : false;
86
101
  /**
@@ -90,6 +105,35 @@ export type IsNever<T> = [T] extends [never] ? true : false;
90
105
  * - Collections can be assigned from their JSON representations
91
106
  */
92
107
  export type AssignableProps<T> = {
93
- [K in NonFunctionPropNames<T>]?: T[K] extends MapSchema<infer U> ? MapSchema<U> | Record<string, U extends Schema ? (U | AssignableProps<U>) : U> : T[K] extends ArraySchema<infer U> ? ArraySchema<U> | (U extends Schema ? (U | AssignableProps<U>)[] : U[]) : T[K] extends SetSchema<infer U> ? SetSchema<U> | Set<U> | (U extends Schema ? (U | AssignableProps<U>)[] : U[]) : T[K] extends CollectionSchema<infer U> ? CollectionSchema<U> | (U extends Schema ? (U | AssignableProps<U>)[] : U[]) : T[K] extends Schema ? T[K] | AssignableProps<T[K]> : T[K];
108
+ [K in NonFunctionPropNames<T>]?: AssignableValue<T[K]>;
109
+ };
110
+ /**
111
+ * Value-level assignment shape shared by `AssignableProps` and
112
+ * `BuilderInitProps`. Captures the "you can pass the real instance, or the
113
+ * plain-object / array shape" pattern.
114
+ */
115
+ export type AssignableValue<V> = V extends MapSchema<infer U> ? MapSchema<U> | Record<string, U extends Schema ? (U | AssignableProps<U>) : U> : V extends ArraySchema<infer U> ? ArraySchema<U> | (U extends Schema ? (U | AssignableProps<U>)[] : U[]) : V extends SetSchema<infer U> ? SetSchema<U> | Set<U> | (U extends Schema ? (U | AssignableProps<U>)[] : U[]) : V extends CollectionSchema<infer U> ? CollectionSchema<U> | (U extends Schema ? (U | AssignableProps<U>)[] : U[]) : V extends Schema ? V | AssignableProps<V> : V;
116
+ export type RefHasDefault<C> = C extends {
117
+ prototype: {
118
+ initialize(...args: infer P): any;
119
+ };
120
+ } ? (P extends readonly [] ? true : false) : true;
121
+ type FieldValue<F> = F extends FieldBuilder<infer V, boolean, boolean> ? V : F extends new (...args: any[]) => infer I ? (I extends Schema ? I : never) : never;
122
+ type KeyClass<T, K extends keyof T> = T[K] extends FieldBuilder<unknown, infer D extends boolean, infer O extends boolean> ? (D extends true ? "optional" : O extends true ? "optional" : "required") : T[K] extends new (...args: any[]) => Schema ? (RefHasDefault<T[K]> extends true ? "optional" : "required") : "none";
123
+ export type BuilderRequiredKeys<T> = {
124
+ [K in keyof T]-?: KeyClass<T, K> extends "required" ? K : never;
125
+ }[keyof T];
126
+ export type BuilderOptionalKeys<T> = {
127
+ [K in keyof T]-?: KeyClass<T, K> extends "optional" ? K : never;
128
+ }[keyof T];
129
+ /**
130
+ * Constructor/init-props type for a schema() fields map. Required fields
131
+ * (primitives without `.default()` or `.optional()`, and Schema refs with
132
+ * non-zero-arg `initialize()`) are `:`; everything else is `?:`.
133
+ */
134
+ export type BuilderInitProps<T> = {
135
+ [K in BuilderRequiredKeys<T>]: AssignableValue<FieldValue<T[K]>>;
136
+ } & {
137
+ [K in BuilderOptionalKeys<T>]?: AssignableValue<Exclude<FieldValue<T[K]>, undefined>>;
94
138
  };
95
139
  export {};
@@ -22,6 +22,7 @@ export interface BuilderDefinition {
22
22
  deprecatedThrows?: boolean;
23
23
  static?: boolean;
24
24
  stream?: boolean;
25
+ optional?: boolean;
25
26
  /** Declaration-scope priority callback for `.stream()` fields. */
26
27
  streamPriority?: (view: any, element: any) => number;
27
28
  }
@@ -32,11 +33,25 @@ export type BuilderOf<T> = FieldBuilder<T>;
32
33
  /**
33
34
  * Chainable field builder. Instances are produced by `t.*()` factories.
34
35
  *
35
- * The generic parameter T is the runtime/JS type of the field (e.g. `number`,
36
- * `string`, `ArraySchema<Item>`). schema() reads the internal configuration
37
- * via `toDefinition()` and wires up metadata through the existing pipeline.
36
+ * Generics:
37
+ * - `T` is the runtime/JS type of the field (e.g. `number`, `string`,
38
+ * `ArraySchema<Item>`). `.optional()` widens it to `T | undefined`
39
+ * so the inferred instance/toJSON shapes reflect absence.
40
+ * - `HasDefault` is a compile-time flag that the field carries a
41
+ * construction-time default — either an explicit `.default(v)` or an
42
+ * auto-default from a collection factory (`t.array`, `t.map`, …) or a
43
+ * Schema ref whose `initialize` takes zero args.
44
+ * - `IsOptional` is a compile-time brand for `.optional()`. Both
45
+ * `HasDefault` and `IsOptional` make the field omittable in
46
+ * `BuilderInitProps<T>`. A separate brand (rather than reading
47
+ * `undefined extends V`) sidesteps a TypeScript quirk where
48
+ * class-generic-inferred `V` resolves `undefined extends V` as `true`
49
+ * even for non-undefined types.
50
+ *
51
+ * schema() reads the internal configuration via `toDefinition()` and wires
52
+ * up metadata through the existing pipeline.
38
53
  */
39
- export declare class FieldBuilder<T = unknown> {
54
+ export declare class FieldBuilder<T = unknown, HasDefault extends boolean = false, IsOptional extends boolean = false> {
40
55
  readonly [$builder]: true;
41
56
  _type: DefinitionType;
42
57
  _default: any;
@@ -49,10 +64,11 @@ export declare class FieldBuilder<T = unknown> {
49
64
  _deprecatedThrows: boolean;
50
65
  _static: boolean;
51
66
  _stream: boolean;
67
+ _optional: boolean;
52
68
  _streamPriority: ((view: any, element: any) => number) | undefined;
53
69
  constructor(type: DefinitionType);
54
70
  /** Provide a default value for this field. */
55
- default(value: T): this;
71
+ default(value: T): FieldBuilder<T, true, IsOptional>;
56
72
  /** Tag this field with a view tag (DEFAULT_VIEW_TAG when called without arg). */
57
73
  view(tag?: number): this;
58
74
  /** Mark this field as owned (encoder-side ownership filtering). */
@@ -107,52 +123,66 @@ export declare class FieldBuilder<T = unknown> {
107
123
  priority<V = any>(fn: (view: any, element: V) => number): this;
108
124
  /** Mark this field as deprecated. Pass `false` to silence the access error. */
109
125
  deprecated(throws?: boolean): this;
126
+ /**
127
+ * Mark this field as optional — inferred instance type becomes
128
+ * `T | undefined` and the property becomes omittable in initialization
129
+ * props. Skips the auto-instantiation of collection / Schema-ref
130
+ * defaults, so the field starts as `undefined` at runtime.
131
+ */
132
+ optional(): FieldBuilder<T | undefined, HasDefault, true>;
110
133
  toDefinition(): BuilderDefinition;
111
134
  }
112
135
  export declare function isBuilder(value: any): value is FieldBuilder<any>;
113
136
  export type ChildType = RawPrimitiveType | Constructor<Schema> | FieldBuilder<any>;
114
137
  interface ArrayFactory {
115
- <C extends Constructor<Schema>>(child: C): FieldBuilder<ArraySchema<InstanceType<C>>>;
116
- <P extends RawPrimitiveType>(child: P): FieldBuilder<ArraySchema<InferValueType<P>>>;
117
- <V>(child: FieldBuilder<V>): FieldBuilder<ArraySchema<V>>;
138
+ <C extends Constructor<Schema>>(child: C): FieldBuilder<ArraySchema<InstanceType<C>>, true, false>;
139
+ <P extends RawPrimitiveType>(child: P): FieldBuilder<ArraySchema<InferValueType<P>>, true, false>;
140
+ <V>(child: FieldBuilder<V>): FieldBuilder<ArraySchema<V>, true, false>;
118
141
  }
119
142
  interface MapFactory {
120
- <C extends Constructor<Schema>>(child: C): FieldBuilder<MapSchema<InstanceType<C>>>;
121
- <P extends RawPrimitiveType>(child: P): FieldBuilder<MapSchema<InferValueType<P>>>;
122
- <V>(child: FieldBuilder<V>): FieldBuilder<MapSchema<V>>;
143
+ <C extends Constructor<Schema>>(child: C): FieldBuilder<MapSchema<InstanceType<C>>, true, false>;
144
+ <P extends RawPrimitiveType>(child: P): FieldBuilder<MapSchema<InferValueType<P>>, true, false>;
145
+ <V>(child: FieldBuilder<V>): FieldBuilder<MapSchema<V>, true, false>;
123
146
  }
124
147
  interface SetFactory {
125
- <C extends Constructor<Schema>>(child: C): FieldBuilder<SetSchema<InstanceType<C>>>;
126
- <P extends RawPrimitiveType>(child: P): FieldBuilder<SetSchema<InferValueType<P>>>;
127
- <V>(child: FieldBuilder<V>): FieldBuilder<SetSchema<V>>;
148
+ <C extends Constructor<Schema>>(child: C): FieldBuilder<SetSchema<InstanceType<C>>, true, false>;
149
+ <P extends RawPrimitiveType>(child: P): FieldBuilder<SetSchema<InferValueType<P>>, true, false>;
150
+ <V>(child: FieldBuilder<V>): FieldBuilder<SetSchema<V>, true, false>;
128
151
  }
129
152
  interface CollectionFactory {
130
- <C extends Constructor<Schema>>(child: C): FieldBuilder<CollectionSchema<InstanceType<C>>>;
131
- <P extends RawPrimitiveType>(child: P): FieldBuilder<CollectionSchema<InferValueType<P>>>;
132
- <V>(child: FieldBuilder<V>): FieldBuilder<CollectionSchema<V>>;
153
+ <C extends Constructor<Schema>>(child: C): FieldBuilder<CollectionSchema<InstanceType<C>>, true, false>;
154
+ <P extends RawPrimitiveType>(child: P): FieldBuilder<CollectionSchema<InferValueType<P>>, true, false>;
155
+ <V>(child: FieldBuilder<V>): FieldBuilder<CollectionSchema<V>, true, false>;
133
156
  }
134
157
  interface StreamFactory {
135
- <C extends Constructor<Schema>>(child: C): FieldBuilder<StreamSchema<InstanceType<C>>>;
158
+ <C extends Constructor<Schema>>(child: C): FieldBuilder<StreamSchema<InstanceType<C>>, true, false>;
159
+ }
160
+ type RefHasDefault<C> = C extends {
161
+ prototype: {
162
+ initialize(...args: infer P): any;
163
+ };
164
+ } ? (P extends readonly [] ? true : false) : true;
165
+ interface RefFactory {
166
+ <C extends Constructor<Schema>>(ctor: C): FieldBuilder<InstanceType<C>, RefHasDefault<C>, false>;
136
167
  }
137
- declare function refFactory<C extends Constructor<Schema>>(ctor: C): FieldBuilder<InstanceType<C>>;
138
168
  export declare const t: Readonly<{
139
- string: () => FieldBuilder<string>;
140
- number: () => FieldBuilder<number>;
141
- boolean: () => FieldBuilder<boolean>;
142
- int8: () => FieldBuilder<number>;
143
- uint8: () => FieldBuilder<number>;
144
- int16: () => FieldBuilder<number>;
145
- uint16: () => FieldBuilder<number>;
146
- int32: () => FieldBuilder<number>;
147
- uint32: () => FieldBuilder<number>;
148
- int64: () => FieldBuilder<number>;
149
- uint64: () => FieldBuilder<number>;
150
- float32: () => FieldBuilder<number>;
151
- float64: () => FieldBuilder<number>;
152
- bigint64: () => FieldBuilder<bigint>;
153
- biguint64: () => FieldBuilder<bigint>;
169
+ string: () => FieldBuilder<string, false, false>;
170
+ number: () => FieldBuilder<number, false, false>;
171
+ boolean: () => FieldBuilder<boolean, false, false>;
172
+ int8: () => FieldBuilder<number, false, false>;
173
+ uint8: () => FieldBuilder<number, false, false>;
174
+ int16: () => FieldBuilder<number, false, false>;
175
+ uint16: () => FieldBuilder<number, false, false>;
176
+ int32: () => FieldBuilder<number, false, false>;
177
+ uint32: () => FieldBuilder<number, false, false>;
178
+ int64: () => FieldBuilder<number, false, false>;
179
+ uint64: () => FieldBuilder<number, false, false>;
180
+ float32: () => FieldBuilder<number, false, false>;
181
+ float64: () => FieldBuilder<number, false, false>;
182
+ bigint64: () => FieldBuilder<bigint, false, false>;
183
+ biguint64: () => FieldBuilder<bigint, false, false>;
154
184
  /** Reference to a Schema subtype. `t.array(Item)` usually reads better, but this is available when a plain ref is needed. */
155
- ref: typeof refFactory;
185
+ ref: RefFactory;
156
186
  array: ArrayFactory;
157
187
  map: MapFactory;
158
188
  set: SetFactory;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@colyseus/schema",
3
- "version": "5.0.0",
3
+ "version": "5.0.2",
4
4
  "description": "Binary state serializer with delta encoding for games",
5
5
  "type": "module",
6
6
  "bin": {
package/src/Metadata.ts CHANGED
@@ -17,6 +17,7 @@ export type MetadataField = {
17
17
  owned?: boolean,
18
18
  static?: boolean,
19
19
  stream?: boolean,
20
+ optional?: boolean,
20
21
  };
21
22
 
22
23
  export type Metadata =
package/src/Reflection.ts CHANGED
@@ -52,8 +52,8 @@ export const Reflection = schema({
52
52
  types: t.array(ReflectionType),
53
53
  rootType: t.number(),
54
54
  }, "Reflection") as ReturnType<typeof schema<{
55
- types: FieldBuilder<ArraySchema<ReflectionType>>;
56
- rootType: FieldBuilder<number>;
55
+ types: FieldBuilder<ArraySchema<ReflectionType>, true, false>;
56
+ rootType: FieldBuilder<number, false, false>;
57
57
  }>> & ReflectionStatic;
58
58
 
59
59
  export type Reflection = SchemaType<typeof Reflection>;
@@ -9,7 +9,7 @@ import { TypeDefinition, getType } from "./types/registry.js";
9
9
  import { OPERATION } from "./encoding/spec.js";
10
10
  import { TypeContext } from "./types/TypeContext.js";
11
11
  import { assertInstanceType, assertType, EncodeSchemaError } from "./encoding/assert.js";
12
- import type { InferValueType, InferSchemaInstanceType, AssignableProps, IsNever } from "./types/HelperTypes.js";
12
+ import type { InferValueType, InferSchemaInstanceType, AssignableProps, BuilderInitProps, IsNever } from "./types/HelperTypes.js";
13
13
  import { CollectionSchema } from "./types/custom/CollectionSchema.js";
14
14
  import { SetSchema } from "./types/custom/SetSchema.js";
15
15
  import { StreamSchema } from "./types/custom/StreamSchema.js";
@@ -529,9 +529,13 @@ export function deprecated(throws: boolean = true): PropertyDecorator {
529
529
  }
530
530
  }
531
531
 
532
- // Helper type to extract InitProps from initialize method
533
- // Supports both single object parameter and multiple parameters
534
- // If no initialize method is specified, use AssignableProps for field initialization
532
+ // Helper type to extract InitProps from initialize method.
533
+ // - Non-empty initialize params: use them directly.
534
+ // - Zero-arg initialize: no args accepted (`never`) user-supplied field
535
+ // values would be dropped at runtime (parent's initialize is skipped
536
+ // during child construction via the `new.target === klass` guard, and
537
+ // own-field auto-assignment happens only inside initialize).
538
+ // - No initialize at all: derive from fields map.
535
539
  type ExtractInitProps<T> = T extends { initialize: (...args: infer P) => void }
536
540
  ? P extends readonly []
537
541
  ? never
@@ -540,28 +544,40 @@ type ExtractInitProps<T> = T extends { initialize: (...args: infer P) => void }
540
544
  ? First
541
545
  : P
542
546
  : P
543
- : AssignableProps<InferSchemaInstanceType<T>>;
547
+ : BuilderInitProps<T>;
544
548
 
545
- // Helper type to determine if InitProps should be required
546
- type IsInitPropsRequired<T> = T extends { initialize: (props: any) => void }
547
- ? true
548
- : T extends { initialize: (...args: infer P) => void }
549
- ? P extends readonly []
550
- ? false
551
- : true
552
- : false;
549
+ // Does the init-props shape have at least one required property?
550
+ type HasRequiredKeys<X> = {} extends X ? false : true;
551
+
552
+ // Whether the constructor's init-props argument must be supplied.
553
+ // Mirrors the cases inside ExtractInitProps: non-empty initialize params
554
+ // are required; zero-arg initialize accepts nothing; no initialize
555
+ // depends on whether the derived BuilderInitProps has any required keys.
556
+ type IsInitPropsRequired<T> = T extends { initialize: (...args: infer P) => void }
557
+ ? P extends readonly []
558
+ ? false
559
+ : true
560
+ : HasRequiredKeys<BuilderInitProps<T>>;
561
+
562
+ // Whether T declares any non-empty `initialize` method. Used to tighten
563
+ // the constructor signature: authors who write an explicit `initialize()`
564
+ // with args opt into strict required args. Without an initialize the sig
565
+ // also allows `[]` so the common `new X(); x.field = ...` pattern works.
566
+ type HasExplicitInit<T> = T extends { initialize: (...args: infer P) => void }
567
+ ? P extends readonly [] ? false : true
568
+ : false;
553
569
 
554
570
  /**
555
571
  * A `schema()` field definition accepts a FieldBuilder, a Schema subclass
556
572
  * (shorthand for `t.ref(Class)`), or a method (attached to the prototype).
557
573
  */
558
- export type FieldsAndMethods = Record<string, FieldBuilder<any> | (new (...args: any[]) => Schema) | Function>;
574
+ export type FieldsAndMethods = Record<string, FieldBuilder<any, boolean, boolean> | (new (...args: any[]) => Schema) | Function>;
559
575
 
560
576
  export interface SchemaWithExtends<T, P extends typeof Schema> {
561
577
  extend: <T2 extends FieldsAndMethods = FieldsAndMethods>(
562
578
  fields: T2 & ThisType<InferSchemaInstanceType<T & T2>>,
563
579
  name?: string,
564
- ) => SchemaWithExtendsConstructor<T & T2, ExtractInitProps<T2>, P>;
580
+ ) => SchemaWithExtendsConstructor<T & T2, ExtractInitProps<T & T2>, P>;
565
581
  }
566
582
 
567
583
  /**
@@ -582,7 +598,22 @@ export interface SchemaWithExtendsConstructor<
582
598
  P extends typeof Schema
583
599
  > extends SchemaWithExtends<T, P> {
584
600
  '~type': InferSchemaInstanceType<T>;
585
- new (...args: [InitProps] extends [never] ? [] : InitProps extends readonly any[] ? InitProps : IsInitPropsRequired<T> extends true ? [InitProps] : [InitProps?]): InferSchemaInstanceType<T> & InstanceType<P>;
601
+ // Constructor signature:
602
+ // - InitProps = never (zero-arg initialize): no args.
603
+ // - InitProps is a tuple (multi-arg initialize): spread it.
604
+ // - Explicit `initialize(arg)` with required args: strict [InitProps]
605
+ // — the author opted into requiring them.
606
+ // - No initialize, but required builder fields: allow `[]` or
607
+ // `[InitProps]`. Preserves `new X(); x.field = ...` while still
608
+ // flagging incomplete-object mistakes like `new X({ hp: 1 })`.
609
+ // - Otherwise: optional single-arg.
610
+ new (...args:
611
+ [InitProps] extends [never] ? []
612
+ : InitProps extends readonly any[] ? InitProps
613
+ : HasExplicitInit<T> extends true ? [InitProps]
614
+ : IsInitPropsRequired<T> extends true ? ([] | [InitProps])
615
+ : [InitProps?]
616
+ ): InferSchemaInstanceType<T> & InstanceType<P>;
586
617
  prototype: InferSchemaInstanceType<T> & InstanceType<P> & {
587
618
  initialize(...args: [InitProps] extends [never] ? [] : InitProps extends readonly any[] ? InitProps : [InitProps]): void;
588
619
  };
@@ -627,6 +658,7 @@ export function schema<
627
658
  const staticFields: string[] = [];
628
659
  const streamFields: string[] = [];
629
660
  const streamPriorityFields: { [field: string]: (view: any, element: any) => number } = {};
661
+ const optionalFields: string[] = [];
630
662
 
631
663
  for (const fieldName in fieldsAndMethods) {
632
664
  const value: any = (fieldsAndMethods as any)[fieldName];
@@ -643,11 +675,13 @@ export function schema<
643
675
  if (def.static) { staticFields.push(fieldName); }
644
676
  if (def.stream) { streamFields.push(fieldName); }
645
677
  if (def.streamPriority !== undefined) { streamPriorityFields[fieldName] = def.streamPriority; }
678
+ if (def.optional) { optionalFields.push(fieldName); }
646
679
 
647
680
  if (def.hasDefault) {
648
681
  defaultValues[fieldName] = def.default;
649
- } else {
682
+ } else if (!def.optional) {
650
683
  // Auto-instantiate collection/Schema defaults when none is provided.
684
+ // `.optional()` opts out — field starts as undefined.
651
685
  const rawType: any = def.type;
652
686
  if (rawType && typeof rawType === "object") {
653
687
  if (rawType.array !== undefined) {
@@ -759,6 +793,13 @@ export function schema<
759
793
  }
760
794
  }
761
795
 
796
+ if (optionalFields.length > 0) {
797
+ const metadata = (klass as any)[Symbol.metadata] as Metadata;
798
+ for (const fieldName of optionalFields) {
799
+ metadata[metadata[fieldName]].optional = true;
800
+ }
801
+ }
802
+
762
803
  if (name) {
763
804
  Object.defineProperty(klass, "name", { value: name });
764
805
  }
package/src/index.ts CHANGED
@@ -71,7 +71,7 @@ export { t, FieldBuilder, isBuilder, type BuilderDefinition, type ChildType } fr
71
71
  export { TypeContext } from "./types/TypeContext.js";
72
72
 
73
73
  // Helper types for type inference
74
- export type { InferValueType, InferSchemaInstanceType, AssignableProps } from "./types/HelperTypes.js";
74
+ export type { InferValueType, InferSchemaInstanceType, AssignableProps, BuilderInitProps } from "./types/HelperTypes.js";
75
75
 
76
76
  export { getDecoderStateCallbacks, type CallbackProxy, type SchemaCallback, type CollectionCallback, type SchemaCallbackProxy } from "./decoder/strategy/getDecoderStateCallbacks.js";
77
77
  export { Callbacks, StateCallbackStrategy } from "./decoder/strategy/Callbacks.js";
@@ -80,12 +80,25 @@ export type InferValueType<T> =
80
80
 
81
81
  : never;
82
82
 
83
+ // Keys whose FieldBuilder generic admits `undefined` (i.e. `.optional()` was chained).
84
+ type OptionalBuilderKeys<T> = {
85
+ [K in keyof T]: T[K] extends FieldBuilder<infer V>
86
+ ? (undefined extends V ? K : never)
87
+ : never
88
+ }[keyof T];
89
+
90
+ type RequiredBuilderKeys<T> = Exclude<keyof T, OptionalBuilderKeys<T>>;
91
+
83
92
  export type InferSchemaInstanceType<T> = {
84
- [K in keyof T]: T[K] extends FieldBuilder<any>
93
+ [K in RequiredBuilderKeys<T>]: T[K] extends FieldBuilder<any>
85
94
  ? InferValueType<T[K]>
86
95
  : T[K] extends (...args: any[]) => any
87
96
  ? (T[K] extends new (...args: any[]) => any ? InferValueType<T[K]> : T[K])
88
97
  : InferValueType<T[K]>
98
+ } & {
99
+ [K in OptionalBuilderKeys<T>]?: T[K] extends FieldBuilder<infer V>
100
+ ? V
101
+ : never
89
102
  } & Schema;
90
103
 
91
104
  export type NonFunctionProps<T> = Omit<T, {
@@ -107,16 +120,28 @@ export type NonFunctionNonPrimitivePropNames<T> = {
107
120
  // Helper to recursively convert Schema instances to their JSON representation
108
121
  type ToJSONValue<U> = U extends Schema ? ToJSON<U> : PrimitiveStringToType<U>;
109
122
 
110
- export type ToJSON<T> = NonFunctionProps<{
111
- [K in keyof T]:
112
- T[K] extends MapSchema<infer U> ? Record<string, ToJSONValue<U>>
113
- : T[K] extends Map<string, infer U> ? Record<string, ToJSONValue<U>>
114
- : T[K] extends ArraySchema<infer U> ? ToJSONValue<U>[]
115
- : T[K] extends SetSchema<infer U> ? ToJSONValue<U>[]
116
- : T[K] extends CollectionSchema<infer U> ? ToJSONValue<U>[]
117
- : T[K] extends Schema ? ToJSON<T[K]>
118
- : T[K]
119
- }>;
123
+ type ToJSONField<X> =
124
+ X extends MapSchema<infer U> ? Record<string, ToJSONValue<U>>
125
+ : X extends Map<string, infer U> ? Record<string, ToJSONValue<U>>
126
+ : X extends ArraySchema<infer U> ? ToJSONValue<U>[]
127
+ : X extends SetSchema<infer U> ? ToJSONValue<U>[]
128
+ : X extends CollectionSchema<infer U> ? ToJSONValue<U>[]
129
+ : X extends Schema ? ToJSON<X>
130
+ : X;
131
+
132
+ // Keys whose value type admits `undefined` — runtime `toJSON()` omits those,
133
+ // so they surface as `?:` on the JSON shape.
134
+ type ToJSONRequiredKeys<T> = {
135
+ [K in keyof T]-?: undefined extends T[K] ? never : K
136
+ }[keyof T];
137
+ type ToJSONOptionalKeys<T> = {
138
+ [K in keyof T]-?: undefined extends T[K] ? K : never
139
+ }[keyof T];
140
+
141
+ export type ToJSON<T> = NonFunctionProps<
142
+ & { [K in ToJSONRequiredKeys<T>]: ToJSONField<T[K]> }
143
+ & { [K in ToJSONOptionalKeys<T>]?: ToJSONField<Exclude<T[K], undefined>> }
144
+ >;
120
145
 
121
146
  // Helper type to check if T is exactly 'never' (meaning no InitProps was provided)
122
147
  export type IsNever<T> = [T] extends [never] ? true : false;
@@ -128,15 +153,75 @@ export type IsNever<T> = [T] extends [never] ? true : false;
128
153
  * - Collections can be assigned from their JSON representations
129
154
  */
130
155
  export type AssignableProps<T> = {
131
- [K in NonFunctionPropNames<T>]?: T[K] extends MapSchema<infer U>
156
+ [K in NonFunctionPropNames<T>]?: AssignableValue<T[K]>
157
+ };
158
+
159
+ /**
160
+ * Value-level assignment shape shared by `AssignableProps` and
161
+ * `BuilderInitProps`. Captures the "you can pass the real instance, or the
162
+ * plain-object / array shape" pattern.
163
+ */
164
+ export type AssignableValue<V> =
165
+ V extends MapSchema<infer U>
132
166
  ? MapSchema<U> | Record<string, U extends Schema ? (U | AssignableProps<U>) : U>
133
- : T[K] extends ArraySchema<infer U>
167
+ : V extends ArraySchema<infer U>
134
168
  ? ArraySchema<U> | (U extends Schema ? (U | AssignableProps<U>)[] : U[])
135
- : T[K] extends SetSchema<infer U>
169
+ : V extends SetSchema<infer U>
136
170
  ? SetSchema<U> | Set<U> | (U extends Schema ? (U | AssignableProps<U>)[] : U[])
137
- : T[K] extends CollectionSchema<infer U>
171
+ : V extends CollectionSchema<infer U>
138
172
  ? CollectionSchema<U> | (U extends Schema ? (U | AssignableProps<U>)[] : U[])
139
- : T[K] extends Schema
140
- ? T[K] | AssignableProps<T[K]>
141
- : T[K]
142
- };
173
+ : V extends Schema
174
+ ? V | AssignableProps<V>
175
+ : V;
176
+
177
+ // ---------------------------------------------------------------------------
178
+ // BuilderInitProps<T> — init-props shape derived from a schema() fields map.
179
+ // Unlike AssignableProps (fully partial, for `.assign()` updates), this type
180
+ // enforces required vs optional based on per-field `HasDefault` + `undefined`.
181
+ // ---------------------------------------------------------------------------
182
+
183
+ // Compile-time analogue of schema()'s Schema-ref auto-default rule:
184
+ // if the ref has no `initialize`, or a zero-arg `initialize`, schema()
185
+ // auto-instantiates it — so the field is omittable at construction.
186
+ export type RefHasDefault<C> =
187
+ C extends { prototype: { initialize(...args: infer P): any } }
188
+ ? (P extends readonly [] ? true : false)
189
+ : true;
190
+
191
+ // Resolve a fields-map entry to its runtime value type.
192
+ type FieldValue<F> =
193
+ F extends FieldBuilder<infer V, boolean, boolean> ? V
194
+ : F extends new (...args: any[]) => infer I ? (I extends Schema ? I : never)
195
+ : never;
196
+
197
+ // Classify each key of a fields map as "required" / "optional" / "none"
198
+ // (methods). Both `HasDefault = true` and the explicit `.optional()` brand
199
+ // `IsOptional = true` mark the field omittable at construction. The brand
200
+ // sidesteps a TypeScript quirk where `undefined extends V` returned `true`
201
+ // for non-undefined V when V was inferred from a class with T in
202
+ // contravariant + covariant positions.
203
+ type KeyClass<T, K extends keyof T> =
204
+ T[K] extends FieldBuilder<unknown, infer D extends boolean, infer O extends boolean>
205
+ ? (D extends true
206
+ ? "optional"
207
+ : O extends true ? "optional" : "required")
208
+ : T[K] extends new (...args: any[]) => Schema
209
+ ? (RefHasDefault<T[K]> extends true ? "optional" : "required")
210
+ : "none";
211
+
212
+ export type BuilderRequiredKeys<T> = {
213
+ [K in keyof T]-?: KeyClass<T, K> extends "required" ? K : never
214
+ }[keyof T];
215
+
216
+ export type BuilderOptionalKeys<T> = {
217
+ [K in keyof T]-?: KeyClass<T, K> extends "optional" ? K : never
218
+ }[keyof T];
219
+
220
+ /**
221
+ * Constructor/init-props type for a schema() fields map. Required fields
222
+ * (primitives without `.default()` or `.optional()`, and Schema refs with
223
+ * non-zero-arg `initialize()`) are `:`; everything else is `?:`.
224
+ */
225
+ export type BuilderInitProps<T> =
226
+ & { [K in BuilderRequiredKeys<T>]: AssignableValue<FieldValue<T[K]>> }
227
+ & { [K in BuilderOptionalKeys<T>]?: AssignableValue<Exclude<FieldValue<T[K]>, undefined>> };