@colyseus/schema 5.0.22 → 5.0.24
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.
- package/build/codegen/cli.cjs +448 -201
- package/build/codegen/cli.cjs.map +1 -1
- package/build/codegen/languages/swift.d.ts +23 -0
- package/build/types/HelperTypes.d.ts +4 -16
- package/package.json +1 -1
- package/src/codegen/api.ts +2 -1
- package/src/codegen/languages/swift.ts +270 -0
- package/src/types/HelperTypes.ts +15 -15
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { File, Context } from "../types.js";
|
|
2
|
+
import { GenerateOptions } from "../api.js";
|
|
3
|
+
export declare const name = "Swift";
|
|
4
|
+
/**
|
|
5
|
+
* Swift Code Generator
|
|
6
|
+
*
|
|
7
|
+
* Emits typed façades over the `Colyseus` package's runtime: one `SchemaRef`
|
|
8
|
+
* subclass per schema, whose properties read through the shared handle on the
|
|
9
|
+
* instance the core decoded. Nothing is copied and nothing is stored, so a
|
|
10
|
+
* generated class stays correct as patches arrive.
|
|
11
|
+
*
|
|
12
|
+
* Collection properties return `MapSchema<T>` / `ArraySchema<T>`, which carry
|
|
13
|
+
* the field they came from — that is what `callbacks.onAdd(state.players, …)`
|
|
14
|
+
* registers against.
|
|
15
|
+
*/
|
|
16
|
+
/**
|
|
17
|
+
* Generate individual files for each class/interface/enum
|
|
18
|
+
*/
|
|
19
|
+
export declare function generate(context: Context, options: GenerateOptions): File[];
|
|
20
|
+
/**
|
|
21
|
+
* Generate a single bundled file containing all classes, interfaces, and enums
|
|
22
|
+
*/
|
|
23
|
+
export declare function renderBundle(context: Context, options: GenerateOptions): File;
|
|
@@ -89,16 +89,10 @@ export type CodecFor<T> = {
|
|
|
89
89
|
[K in RawPrimitiveType]: [T] extends [InferValueType<K>] ? K : never;
|
|
90
90
|
}[RawPrimitiveType];
|
|
91
91
|
type IsOptionalBuilderKey<T, K extends keyof T> = T[K] extends FieldBuilder<unknown, boolean, infer O extends boolean> ? O : false;
|
|
92
|
-
type OptionalBuilderKeys<T> = {
|
|
93
|
-
[K in keyof T]-?: IsOptionalBuilderKey<T, K> extends true ? K : never;
|
|
94
|
-
}[keyof T];
|
|
95
|
-
type RequiredBuilderKeys<T> = {
|
|
96
|
-
[K in keyof T]-?: IsOptionalBuilderKey<T, K> extends true ? never : K;
|
|
97
|
-
}[keyof T];
|
|
98
92
|
export type InferSchemaInstanceType<T> = {
|
|
99
|
-
[K in
|
|
93
|
+
-readonly [K in keyof T as IsOptionalBuilderKey<T, K> extends true ? never : K]-?: 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]>;
|
|
100
94
|
} & {
|
|
101
|
-
[K in
|
|
95
|
+
-readonly [K in keyof T as IsOptionalBuilderKey<T, K> extends true ? K : never]?: T[K] extends FieldBuilder<infer V> ? V : never;
|
|
102
96
|
} & Schema;
|
|
103
97
|
type DataKey<T, K extends keyof T> = K extends keyof Schema ? never : T[K] extends Function ? never : K;
|
|
104
98
|
export type NonFunctionPropNames<T> = {
|
|
@@ -110,16 +104,10 @@ export type NonFunctionNonPrimitivePropNames<T> = {
|
|
|
110
104
|
type ToJSONValue<U> = U extends Schema ? ToJSON<U> : PrimitiveStringToType<U>;
|
|
111
105
|
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;
|
|
112
106
|
type IsOptionalKey<T, K extends keyof T> = undefined extends {} ? ({} extends Pick<T, K> ? true : false) : (undefined extends T[K] ? true : false);
|
|
113
|
-
type ToJSONRequiredKeys<T> = {
|
|
114
|
-
[K in keyof T]-?: [DataKey<T, K>] extends [never] ? never : IsOptionalKey<T, K> extends true ? never : K;
|
|
115
|
-
}[keyof T];
|
|
116
|
-
type ToJSONOptionalKeys<T> = {
|
|
117
|
-
[K in keyof T]-?: [DataKey<T, K>] extends [never] ? never : IsOptionalKey<T, K> extends true ? K : never;
|
|
118
|
-
}[keyof T];
|
|
119
107
|
export type ToJSON<T> = {
|
|
120
|
-
[K in
|
|
108
|
+
-readonly [K in keyof T as IsOptionalKey<T, K> extends true ? never : DataKey<T, K>]-?: ToJSONField<T[K]>;
|
|
121
109
|
} & {
|
|
122
|
-
[K in
|
|
110
|
+
-readonly [K in keyof T as IsOptionalKey<T, K> extends true ? DataKey<T, K> : never]?: ToJSONField<Exclude<T[K], undefined>>;
|
|
123
111
|
};
|
|
124
112
|
/**
|
|
125
113
|
* The plain DATA shape of a Schema instance type `T`: its synchronized fields
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@colyseus/schema",
|
|
3
|
-
"version": "5.0.
|
|
3
|
+
"version": "5.0.24",
|
|
4
4
|
"description": "Automatic state replication for multiplayer games. Mutate plain objects, and every client gets a live, typed mirror through delta encoding.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
package/src/codegen/api.ts
CHANGED
|
@@ -15,8 +15,9 @@ import * as lua from "./languages/lua.js";
|
|
|
15
15
|
import * as c from "./languages/c.js";
|
|
16
16
|
import * as gdscript from "./languages/gdscript.js";
|
|
17
17
|
import * as dart from "./languages/dart.js";
|
|
18
|
+
import * as swift from "./languages/swift.js";
|
|
18
19
|
|
|
19
|
-
export const generators: Record<string, any> = { csharp, cpp, haxe, ts, js, java, lua, c, gdscript, dart, };
|
|
20
|
+
export const generators: Record<string, any> = { csharp, cpp, haxe, ts, js, java, lua, c, gdscript, dart, swift, };
|
|
20
21
|
|
|
21
22
|
export interface GenerateOptions {
|
|
22
23
|
files: string[],
|
|
@@ -0,0 +1,270 @@
|
|
|
1
|
+
import {
|
|
2
|
+
Class,
|
|
3
|
+
Property,
|
|
4
|
+
File,
|
|
5
|
+
getCommentHeader,
|
|
6
|
+
Interface,
|
|
7
|
+
Enum,
|
|
8
|
+
Context,
|
|
9
|
+
} from "../types.js";
|
|
10
|
+
import { GenerateOptions } from "../api.js";
|
|
11
|
+
|
|
12
|
+
export const name = "Swift";
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Swift types for interface (plain message) properties. Schema getters do not
|
|
16
|
+
* use this table: the `Colyseus` package reads every numeric field as `Double`
|
|
17
|
+
* through `SchemaView`, so all numeric schema types collapse there.
|
|
18
|
+
*/
|
|
19
|
+
const typeMaps: { [key: string]: string } = {
|
|
20
|
+
"string": "String",
|
|
21
|
+
"number": "Double",
|
|
22
|
+
"boolean": "Bool",
|
|
23
|
+
"int8": "Int",
|
|
24
|
+
"uint8": "Int",
|
|
25
|
+
"int16": "Int",
|
|
26
|
+
"uint16": "Int",
|
|
27
|
+
"int32": "Int",
|
|
28
|
+
"uint32": "Int",
|
|
29
|
+
"int64": "Int",
|
|
30
|
+
"uint64": "Int",
|
|
31
|
+
"float32": "Double",
|
|
32
|
+
"float64": "Double",
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const enumNames = new Set<string>();
|
|
36
|
+
|
|
37
|
+
const COMMON_IMPORTS = `import Colyseus`;
|
|
38
|
+
|
|
39
|
+
const distinct = (value: string, index: number, self: string[]) =>
|
|
40
|
+
self.indexOf(value) === index;
|
|
41
|
+
|
|
42
|
+
const isSchemaType = (childType: string) =>
|
|
43
|
+
childType !== undefined && /^[A-Z]/.test(childType) && !enumNames.has(childType);
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Swift Code Generator
|
|
47
|
+
*
|
|
48
|
+
* Emits typed façades over the `Colyseus` package's runtime: one `SchemaRef`
|
|
49
|
+
* subclass per schema, whose properties read through the shared handle on the
|
|
50
|
+
* instance the core decoded. Nothing is copied and nothing is stored, so a
|
|
51
|
+
* generated class stays correct as patches arrive.
|
|
52
|
+
*
|
|
53
|
+
* Collection properties return `MapSchema<T>` / `ArraySchema<T>`, which carry
|
|
54
|
+
* the field they came from — that is what `callbacks.onAdd(state.players, …)`
|
|
55
|
+
* registers against.
|
|
56
|
+
*/
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Generate individual files for each class/interface/enum
|
|
60
|
+
*/
|
|
61
|
+
export function generate(context: Context, options: GenerateOptions): File[] {
|
|
62
|
+
context.enums.forEach((structure) => enumNames.add(structure.name));
|
|
63
|
+
|
|
64
|
+
return [
|
|
65
|
+
...context.classes.map(klass => ({
|
|
66
|
+
name: `${klass.name}.swift`,
|
|
67
|
+
content: generateFile(generateClassBody(klass, context.classes, !!options.namespace), options)
|
|
68
|
+
})),
|
|
69
|
+
...context.interfaces.map(structure => ({
|
|
70
|
+
name: `${structure.name}.swift`,
|
|
71
|
+
content: generateFile(generateInterfaceBody(structure), options),
|
|
72
|
+
})),
|
|
73
|
+
...context.enums.filter(structure => structure.name !== 'OPERATION').map((structure) => ({
|
|
74
|
+
name: `${structure.name}.swift`,
|
|
75
|
+
content: generateFile(generateEnumBody(structure), options),
|
|
76
|
+
})),
|
|
77
|
+
];
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Generate a single bundled file containing all classes, interfaces, and enums
|
|
82
|
+
*/
|
|
83
|
+
export function renderBundle(context: Context, options: GenerateOptions): File {
|
|
84
|
+
const fileName = options.namespace ? `${options.namespace}.swift` : "Schema.swift";
|
|
85
|
+
|
|
86
|
+
context.enums.forEach((structure) => enumNames.add(structure.name));
|
|
87
|
+
|
|
88
|
+
const bodies = [
|
|
89
|
+
...context.classes.map(klass => generateClassBody(klass, context.classes, !!options.namespace)),
|
|
90
|
+
...context.interfaces.map(iface => generateInterfaceBody(iface)),
|
|
91
|
+
...context.enums
|
|
92
|
+
.filter(structure => structure.name !== 'OPERATION')
|
|
93
|
+
.map(e => generateEnumBody(e)),
|
|
94
|
+
].join("\n\n");
|
|
95
|
+
|
|
96
|
+
return { name: fileName, content: generateFile(bodies, options) };
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Swift has no namespaces, so one stands in as a caseless enum. Declarations
|
|
101
|
+
* go inside it through an extension, which works the same whether they are
|
|
102
|
+
* bundled into one file or split across many.
|
|
103
|
+
*/
|
|
104
|
+
function generateFile(body: string, options: GenerateOptions): string {
|
|
105
|
+
const header = `${getCommentHeader()}
|
|
106
|
+
|
|
107
|
+
${COMMON_IMPORTS}
|
|
108
|
+
`;
|
|
109
|
+
|
|
110
|
+
if (!options.namespace) {
|
|
111
|
+
return `${header}
|
|
112
|
+
${body}
|
|
113
|
+
`;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
return `${header}
|
|
117
|
+
public enum ${options.namespace} {}
|
|
118
|
+
|
|
119
|
+
extension ${options.namespace} {
|
|
120
|
+
${indent(body)}
|
|
121
|
+
}
|
|
122
|
+
`;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function indent(text: string): string {
|
|
126
|
+
return text
|
|
127
|
+
.split("\n")
|
|
128
|
+
.map(line => (line.length > 0 ? ` ${line}` : line))
|
|
129
|
+
.join("\n");
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function generateClassBody(klass: Class, allClasses: Class[], namespaced: boolean): string {
|
|
133
|
+
// A class nobody extends is final. One that is extended stays open so a
|
|
134
|
+
// consumer in another module can subclass it — except inside a namespace,
|
|
135
|
+
// where `open` conflicts with the extension's own access level and the
|
|
136
|
+
// subclass is generated alongside it anyway.
|
|
137
|
+
const isExtended = allClasses.some(other => other.extends === klass.name);
|
|
138
|
+
const modifier = isExtended ? (namespaced ? "public" : "open") : "public final";
|
|
139
|
+
const parent = (klass.extends === "Schema") ? "SchemaRef" : klass.extends;
|
|
140
|
+
|
|
141
|
+
const properties = klass.properties
|
|
142
|
+
.map(prop => generateProperty(prop))
|
|
143
|
+
.filter(Boolean)
|
|
144
|
+
.join("\n");
|
|
145
|
+
|
|
146
|
+
// Swift does not carry an `@unchecked Sendable` conformance across module
|
|
147
|
+
// boundaries, so every subclass has to restate it. What it asserts is the
|
|
148
|
+
// SDK's own contract: decoded state is read where it is pumped.
|
|
149
|
+
return `${modifier} class ${klass.name}: ${parent}, @unchecked Sendable {
|
|
150
|
+
${properties}
|
|
151
|
+
}`;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* The Swift type a scalar schema field reads as, or undefined when the field
|
|
156
|
+
* can only be read dynamically (enum-typed and unknown types).
|
|
157
|
+
*/
|
|
158
|
+
function scalarSwiftType(type: string): string | undefined {
|
|
159
|
+
if (type === "string") { return "String"; }
|
|
160
|
+
if (type === "boolean") { return "Bool"; }
|
|
161
|
+
if (typeMaps[type] === "Double" || typeMaps[type] === "Int" || type === "quantized" || type === "number") {
|
|
162
|
+
return "Double";
|
|
163
|
+
}
|
|
164
|
+
return undefined;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function generateProperty(prop: Property): string {
|
|
168
|
+
const deprecation = (prop.deprecated)
|
|
169
|
+
? ` @available(*, deprecated, message: "field '${prop.name}' is deprecated.")\n`
|
|
170
|
+
: '';
|
|
171
|
+
|
|
172
|
+
const escaped = escapeName(prop.name);
|
|
173
|
+
let body: string;
|
|
174
|
+
|
|
175
|
+
if (prop.childType && isSchemaType(prop.childType)) {
|
|
176
|
+
if (prop.type === "ref") {
|
|
177
|
+
body = ` public var ${escaped}: ${prop.childType}? { refOf("${prop.name}") }`;
|
|
178
|
+
} else if (prop.type === "map") {
|
|
179
|
+
body = ` public var ${escaped}: MapSchema<${prop.childType}> { mapOf("${prop.name}") }`;
|
|
180
|
+
} else {
|
|
181
|
+
body = ` public var ${escaped}: ArraySchema<${prop.childType}> { arrayOf("${prop.name}") }`;
|
|
182
|
+
}
|
|
183
|
+
} else if (prop.childType) {
|
|
184
|
+
// A collection of primitives. Everything numeric reads as Double, the
|
|
185
|
+
// same collapse the scalar getters make.
|
|
186
|
+
const child = typeMaps[prop.childType] === "String" ? "String" : "Double";
|
|
187
|
+
if (prop.type === "map") {
|
|
188
|
+
body = ` public var ${escaped}: MapSchema<${child}> { mapOf("${prop.name}") }`;
|
|
189
|
+
} else if (prop.type === "array") {
|
|
190
|
+
body = ` public var ${escaped}: ArraySchema<${child}> { arrayOf("${prop.name}") }`;
|
|
191
|
+
} else {
|
|
192
|
+
// A "ref" with a primitive child has no typed shape to offer.
|
|
193
|
+
body = ` public var ${escaped}: Double { view["${prop.name}"] }`;
|
|
194
|
+
}
|
|
195
|
+
} else {
|
|
196
|
+
const swiftType = scalarSwiftType(prop.type);
|
|
197
|
+
if (swiftType === "String") {
|
|
198
|
+
body = ` public var ${escaped}: String { view.string("${prop.name}") ?? "" }`;
|
|
199
|
+
} else if (swiftType === "Bool") {
|
|
200
|
+
body = ` public var ${escaped}: Bool { view.bool("${prop.name}") }`;
|
|
201
|
+
} else if (swiftType === "Double") {
|
|
202
|
+
body = ` public var ${escaped}: Double { view["${prop.name}"] }`;
|
|
203
|
+
} else {
|
|
204
|
+
// Enum-typed or unknown: read as the number the wire carries.
|
|
205
|
+
body = ` public var ${escaped}: Double { view["${prop.name}"] }`;
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
return deprecation + body;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/**
|
|
213
|
+
* Message payloads are plain structs rather than façades: they arrive as
|
|
214
|
+
* msgpack, not as decoded schema state.
|
|
215
|
+
*/
|
|
216
|
+
function generateInterfaceBody(struct: Interface): string {
|
|
217
|
+
const fields = struct.properties
|
|
218
|
+
.map(prop => ` public var ${escapeName(prop.name)}: ${getInterfaceType(prop)}?`)
|
|
219
|
+
.join("\n");
|
|
220
|
+
|
|
221
|
+
return `public struct ${struct.name}: Codable {
|
|
222
|
+
${fields}
|
|
223
|
+
|
|
224
|
+
public init() {}
|
|
225
|
+
}`;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
function getInterfaceType(prop: Property): string {
|
|
229
|
+
if (prop.type === "array") {
|
|
230
|
+
return `[${typeMaps[prop.childType] ?? prop.childType ?? "Double"}]`;
|
|
231
|
+
}
|
|
232
|
+
return typeMaps[prop.type] ?? prop.type ?? "Double";
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
/**
|
|
236
|
+
* A namespace of constants rather than a Swift enum: Colyseus enums may carry
|
|
237
|
+
* string or floating-point values, and a Swift enum's raw type has to be one
|
|
238
|
+
* or the other.
|
|
239
|
+
*/
|
|
240
|
+
function generateEnumBody(_enum: Enum): string {
|
|
241
|
+
const members = _enum.properties
|
|
242
|
+
.map((prop, i) => {
|
|
243
|
+
let value: string;
|
|
244
|
+
if (prop.type) {
|
|
245
|
+
value = isNaN(Number(prop.type)) ? `"${prop.type}"` : `${Number(prop.type)}`;
|
|
246
|
+
} else {
|
|
247
|
+
value = `${i}`;
|
|
248
|
+
}
|
|
249
|
+
return ` public static let ${escapeName(prop.name)} = ${value}`;
|
|
250
|
+
})
|
|
251
|
+
.join("\n");
|
|
252
|
+
|
|
253
|
+
return `public enum ${_enum.name} {
|
|
254
|
+
${members}
|
|
255
|
+
}`;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
/** Field names come from the server schema and may collide with a keyword. */
|
|
259
|
+
const SWIFT_KEYWORDS = new Set([
|
|
260
|
+
"associatedtype", "class", "deinit", "enum", "extension", "fileprivate", "func", "import",
|
|
261
|
+
"init", "inout", "internal", "let", "open", "operator", "private", "precedencegroup",
|
|
262
|
+
"protocol", "public", "rethrows", "static", "struct", "subscript", "typealias", "var",
|
|
263
|
+
"break", "case", "catch", "continue", "default", "defer", "do", "else", "fallthrough",
|
|
264
|
+
"for", "guard", "if", "in", "repeat", "return", "throw", "switch", "where", "while",
|
|
265
|
+
"Any", "as", "await", "false", "is", "nil", "self", "Self", "super", "throws", "true", "try",
|
|
266
|
+
]);
|
|
267
|
+
|
|
268
|
+
function escapeName(name: string): string {
|
|
269
|
+
return SWIFT_KEYWORDS.has(name) ? `\`${name}\`` : name;
|
|
270
|
+
}
|
package/src/types/HelperTypes.ts
CHANGED
|
@@ -117,19 +117,19 @@ export type CodecFor<T> = {
|
|
|
117
117
|
type IsOptionalBuilderKey<T, K extends keyof T> =
|
|
118
118
|
T[K] extends FieldBuilder<unknown, boolean, infer O extends boolean> ? O : false;
|
|
119
119
|
|
|
120
|
-
//
|
|
121
|
-
//
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
120
|
+
// Split in the `as` clause, never over a precomputed key union: only a
|
|
121
|
+
// homomorphic mapping carries each key back to its declaration in the
|
|
122
|
+
// `schema({ … })` literal, which is what go-to-definition and rename resolve
|
|
123
|
+
// through. `-readonly`, and `-?` on the required half, drop the modifiers a
|
|
124
|
+
// homomorphic mapping inherits from the fields map.
|
|
125
125
|
export type InferSchemaInstanceType<T> = {
|
|
126
|
-
[K in
|
|
126
|
+
-readonly [K in keyof T as IsOptionalBuilderKey<T, K> extends true ? never : K]-?: T[K] extends FieldBuilder<any>
|
|
127
127
|
? InferValueType<T[K]>
|
|
128
128
|
: T[K] extends (...args: any[]) => any
|
|
129
129
|
? (T[K] extends new (...args: any[]) => any ? InferValueType<T[K]> : T[K])
|
|
130
130
|
: InferValueType<T[K]>
|
|
131
131
|
} & {
|
|
132
|
-
[K in
|
|
132
|
+
-readonly [K in keyof T as IsOptionalBuilderKey<T, K> extends true ? K : never]?: T[K] extends FieldBuilder<infer V>
|
|
133
133
|
? V
|
|
134
134
|
: never
|
|
135
135
|
} & Schema;
|
|
@@ -170,15 +170,15 @@ type IsOptionalKey<T, K extends keyof T> = undefined extends {}
|
|
|
170
170
|
? ({} extends Pick<T, K> ? true : false)
|
|
171
171
|
: (undefined extends T[K] ? true : false);
|
|
172
172
|
|
|
173
|
-
//
|
|
174
|
-
|
|
175
|
-
type
|
|
176
|
-
|
|
177
|
-
//
|
|
178
|
-
//
|
|
173
|
+
// Both filters live in the `as` clause, for two independent reasons:
|
|
174
|
+
// navigation, as above — and depth, because the clause runs before the value
|
|
175
|
+
// type, so `ToJSONField` never reaches the machinery, where it would recurse
|
|
176
|
+
// through `restore(json: ToJSON<this>)` past TypeScript 7's instantiation limit.
|
|
177
|
+
// Probing `IsOptionalKey` first is deliberate: `DataKey` then runs in the taken
|
|
178
|
+
// branch only, once per key rather than once per half.
|
|
179
179
|
export type ToJSON<T> =
|
|
180
|
-
& { [K in
|
|
181
|
-
& { [K in
|
|
180
|
+
& { -readonly [K in keyof T as IsOptionalKey<T, K> extends true ? never : DataKey<T, K>]-?: ToJSONField<T[K]> }
|
|
181
|
+
& { -readonly [K in keyof T as IsOptionalKey<T, K> extends true ? DataKey<T, K> : never]?: ToJSONField<Exclude<T[K], undefined>> };
|
|
182
182
|
|
|
183
183
|
/**
|
|
184
184
|
* The plain DATA shape of a Schema instance type `T`: its synchronized fields
|