@oscarpalmer/jhunal 0.18.0 → 0.20.0

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.
@@ -3,10 +3,23 @@ import { ValueName } from "./misc.model.mjs";
3
3
  import { GenericCallback } from "@oscarpalmer/atoms/models";
4
4
 
5
5
  //#region src/models/validation.model.d.ts
6
- type ReportingInformation = Record<ReportingType, boolean>;
6
+ /**
7
+ * Maps each {@link ReportingType} to a boolean flag
8
+ */
9
+ type ReportingInformation = Record<ReportingType, boolean> & {
10
+ type: ReportingType;
11
+ };
12
+ /**
13
+ * Controls how validation failures are reported
14
+ *
15
+ * - `'none'` — returns a boolean _(default)_
16
+ * - `'first'` — returns the first failure as a `Result`
17
+ * - `'all'` — returns all failures as a `Result` _(from same level)_
18
+ * - `'throw'` — throws a {@link ValidationError} on failure
19
+ */
7
20
  type ReportingType = 'all' | 'first' | 'none' | 'throw';
8
21
  /**
9
- * A custom error class for schematic validation failures
22
+ * Thrown when a schema definition is invalid
10
23
  */
11
24
  declare class SchematicError extends Error {
12
25
  constructor(message: string);
@@ -43,16 +56,12 @@ type ValidatedProperty = {
43
56
  validators: ValidatedPropertyValidators;
44
57
  };
45
58
  /**
46
- * Property name in schema
59
+ * The full and short forms of a property's key path
60
+ *
61
+ * For a nested property `address.street`: `full` is `'address.street'`, `short` is `'street'`
47
62
  */
48
63
  type ValidatedPropertyKey = {
49
- /**
50
- * Full property key, including parent keys for nested properties _(e.g., `address.street`)_
51
- */
52
64
  full: string;
53
- /**
54
- * The last segment of the property key _(e.g., `street` for `address.street`)_
55
- */
56
65
  short: string;
57
66
  };
58
67
  /**
@@ -67,15 +76,42 @@ type ValidatedPropertyType = GenericCallback | ValidatedProperty[] | Schematic<u
67
76
  * Each key holds an array of validator functions that receive an `unknown` value and return a `boolean`
68
77
  */
69
78
  type ValidatedPropertyValidators = { [Key in ValueName]?: Array<(value: unknown) => boolean> };
79
+ /**
80
+ * Thrown in `'throw'` mode when one or more properties fail validation; `information` holds all failures
81
+ */
70
82
  declare class ValidationError extends Error {
71
83
  readonly information: ValidationInformation[];
72
84
  constructor(information: ValidationInformation[]);
73
85
  }
86
+ /**
87
+ * Describes a single validation failure
88
+ */
74
89
  type ValidationInformation = {
75
- key: ValidationInformationKey;
76
- message: string;
77
- validator?: GenericCallback;
90
+ /** The key path of the property that failed */key: ValidationInformationKey; /** Human-readable description of the failure */
91
+ message: string; /** The validator function that failed, if the failure was from a `$validators` entry */
92
+ validator?: GenericCallback; /** The value that was provided */
93
+ value: unknown;
78
94
  };
95
+ /**
96
+ * Same shape as {@link ValidatedPropertyKey}; the key path of a failed property
97
+ */
79
98
  type ValidationInformationKey = ValidatedPropertyKey;
99
+ /**
100
+ * Options for validation
101
+ */
102
+ type ValidationOptions<Errors extends ReportingType> = {
103
+ /**
104
+ * How should validation failures be reported; see {@link ReportingType} _(defaults to `'none'`)_
105
+ */
106
+ errors?: Errors;
107
+ /**
108
+ * Validate if unknown keys are present in the object? _(defaults to `false`)_
109
+ */
110
+ strict?: boolean;
111
+ };
112
+ type ValidationOptionsExtended = {
113
+ reporting: ReportingInformation;
114
+ strict: boolean;
115
+ };
80
116
  //#endregion
81
- export { ReportingInformation, ReportingType, SchematicError, ValidatedProperty, ValidatedPropertyKey, ValidatedPropertyType, ValidatedPropertyValidators, ValidationError, ValidationInformation, ValidationInformationKey };
117
+ export { ReportingInformation, ReportingType, SchematicError, ValidatedProperty, ValidatedPropertyKey, ValidatedPropertyType, ValidatedPropertyValidators, ValidationError, ValidationInformation, ValidationInformationKey, ValidationOptions, ValidationOptionsExtended };
@@ -2,7 +2,7 @@ import { NAME_ERROR_SCHEMATIC, NAME_ERROR_VALIDATION } from "../constants.mjs";
2
2
  import { join } from "@oscarpalmer/atoms/string";
3
3
  //#region src/models/validation.model.ts
4
4
  /**
5
- * A custom error class for schematic validation failures
5
+ * Thrown when a schema definition is invalid
6
6
  */
7
7
  var SchematicError = class extends Error {
8
8
  constructor(message) {
@@ -10,6 +10,9 @@ var SchematicError = class extends Error {
10
10
  this.name = NAME_ERROR_SCHEMATIC;
11
11
  }
12
12
  };
13
+ /**
14
+ * Thrown in `'throw'` mode when one or more properties fail validation; `information` holds all failures
15
+ */
13
16
  var ValidationError = class extends Error {
14
17
  constructor(information) {
15
18
  super(join(information.map((item) => item.message), "; "));
@@ -1,8 +1,9 @@
1
1
  import { Infer } from "./models/infer.model.mjs";
2
2
  import { TypedSchema } from "./models/schema.typed.model.mjs";
3
- import { ValidatedProperty } from "./models/validation.model.mjs";
3
+ import { ValidatedProperty, ValidationInformation, ValidationOptions } from "./models/validation.model.mjs";
4
4
  import { Schema } from "./models/schema.plain.model.mjs";
5
5
  import { PlainObject } from "@oscarpalmer/atoms/models";
6
+ import { Result } from "@oscarpalmer/atoms/result/models";
6
7
 
7
8
  //#region src/schematic.d.ts
8
9
  /**
@@ -17,18 +18,64 @@ declare class Schematic<Model> {
17
18
  *
18
19
  * Will assert that the values matches the schema and throw an error if it does not. The error will contain all validation information for the first property that fails validation.
19
20
  * @param value Value to validate
20
- * @param errors Throws an error for the first validation failure
21
+ * @param options Validation options
22
+ * @returns `true` if the value matches the schema, otherwise throws an error
23
+ */
24
+ is(value: unknown, options: ValidationOptions<'throw'>): asserts value is Model;
25
+ /**
26
+ * Does the value match the schema?
27
+ *
28
+ * Will assert that the values matches the schema and throw an error if it does not. The error will contain all validation information for the first property that fails validation.
29
+ * @param value Value to validate
30
+ * @param errors Reporting type
21
31
  * @returns `true` if the value matches the schema, otherwise throws an error
22
32
  */
23
33
  is(value: unknown, errors: 'throw'): asserts value is Model;
34
+ /**
35
+ * Does the value match the schema?
36
+ *
37
+ * Will validate that the value matches the schema and return a result of `true` or all validation information for validation failures from the same depth in the object.
38
+ * @param value Value to validate
39
+ * @param options Validation options
40
+ * @returns `true` if the value matches the schema, otherwise `false`
41
+ */
42
+ is(value: unknown, options: ValidationOptions<'all'>): Result<true, ValidationInformation[]>;
43
+ /**
44
+ * Does the value match the schema?
45
+ *
46
+ * Will validate that the value matches the schema and return a result of `true` or all validation information for validation failures from the same depth in the object.
47
+ * @param value Value to validate
48
+ * @param errors Reporting type
49
+ * @returns `true` if the value matches the schema, otherwise `false`
50
+ */
51
+ is(value: unknown, errors: 'all'): Result<true, ValidationInformation[]>;
52
+ /**
53
+ * Does the value match the schema?
54
+ *
55
+ * Will validate that the value matches the schema and return a result of `true` or all validation information for the failing property.
56
+ * @param value Value to validate
57
+ * @param options Validation options
58
+ * @returns `true` if the value matches the schema, otherwise `false`
59
+ */
60
+ is(value: unknown, options: ValidationOptions<'first'>): Result<true, ValidationInformation>;
61
+ /**
62
+ * Does the value match the schema?
63
+ *
64
+ * Will validate that the value matches the schema and return a result of `true` or all validation information for the failing property.
65
+ * @param value Value to validate
66
+ * @param errors Reporting type
67
+ * @returns `true` if the value matches the schema, otherwise `false`
68
+ */
69
+ is(value: unknown, errors: 'first'): Result<true, ValidationInformation>;
24
70
  /**
25
71
  * Does the value match the schema?
26
72
  *
27
73
  * Will validate that the value matches the schema and return `true` or `false`, without any validation information for validation failures.
28
74
  * @param value Value to validate
75
+ * @param strict Validate if unknown keys are present in the object? _(defaults to `false`)_
29
76
  * @returns `true` if the value matches the schema, otherwise `false`
30
77
  */
31
- is(value: unknown): value is Model;
78
+ is(value: unknown, strict?: true): value is Model;
32
79
  }
33
80
  /**
34
81
  * Create a schematic from a schema
@@ -46,5 +93,6 @@ declare function schematic<Model extends Schema>(schema: Model): Schematic<Infer
46
93
  * @returns A schematic for the given typed schema
47
94
  */
48
95
  declare function schematic<Model extends PlainObject>(schema: TypedSchema<Model>): Schematic<Model>;
96
+ declare const schematicProperties: WeakMap<Schematic<unknown>, ValidatedProperty[]>;
49
97
  //#endregion
50
- export { Schematic, schematic };
98
+ export { Schematic, schematic, schematicProperties };
@@ -1,9 +1,10 @@
1
1
  import { PROPERTY_SCHEMATIC, SCHEMATIC_MESSAGE_SCHEMA_INVALID_TYPE } from "./constants.mjs";
2
- import { getReporting, isSchematic } from "./helpers.mjs";
2
+ import { getOptions, isSchematic } from "./helpers.mjs";
3
3
  import { SchematicError } from "./models/validation.model.mjs";
4
4
  import { getProperties } from "./validation/property.validation.mjs";
5
5
  import { validateObject } from "./validation/value.validation.mjs";
6
6
  import { isPlainObject } from "@oscarpalmer/atoms/is";
7
+ import { error } from "@oscarpalmer/atoms/result/misc";
7
8
  //#region src/schematic.ts
8
9
  /**
9
10
  * A schematic for validating objects
@@ -13,9 +14,16 @@ var Schematic = class {
13
14
  constructor(properties) {
14
15
  Object.defineProperty(this, PROPERTY_SCHEMATIC, { value: true });
15
16
  this.#properties = properties;
17
+ schematicProperties.set(this, properties);
16
18
  }
17
- is(value, errors) {
18
- return validateObject(value, this.#properties, getReporting(errors));
19
+ is(value, options) {
20
+ const { reporting, strict } = getOptions(options);
21
+ const result = validateObject(value, this.#properties, {
22
+ reporting,
23
+ strict
24
+ });
25
+ if (typeof result === "boolean") return result;
26
+ return error(reporting.all ? result : result[0]);
19
27
  }
20
28
  };
21
29
  function schematic(schema) {
@@ -23,5 +31,6 @@ function schematic(schema) {
23
31
  if (!isPlainObject(schema)) throw new SchematicError(SCHEMATIC_MESSAGE_SCHEMA_INVALID_TYPE);
24
32
  return new Schematic(getProperties(schema));
25
33
  }
34
+ const schematicProperties = /* @__PURE__ */ new WeakMap();
26
35
  //#endregion
27
- export { Schematic, schematic };
36
+ export { Schematic, schematic, schematicProperties };
@@ -1,6 +1,6 @@
1
- import { ReportingInformation, ValidatedProperty, ValidationInformation } from "../models/validation.model.mjs";
1
+ import { ValidatedProperty, ValidationInformation, ValidationOptionsExtended } from "../models/validation.model.mjs";
2
2
 
3
3
  //#region src/validation/value.validation.d.ts
4
- declare function validateObject(obj: unknown, properties: ValidatedProperty[], reporting: ReportingInformation, validation?: ValidationInformation[]): boolean;
4
+ declare function validateObject(obj: unknown, properties: ValidatedProperty[], options: ValidationOptionsExtended, origin?: ValidatedProperty, validation?: ValidationInformation[]): boolean | ValidationInformation[];
5
5
  //#endregion
6
6
  export { validateObject };
@@ -1,6 +1,9 @@
1
- import { getInvalidInputMessage, getInvalidMissingMessage, getInvalidTypeMessage, getInvalidValidatorMessage, isSchematic } from "../helpers.mjs";
1
+ import "../constants.mjs";
2
+ import { getInvalidInputMessage, getInvalidMissingMessage, getInvalidTypeMessage, getInvalidValidatorMessage, getUnknownKeysMessage, isSchematic } from "../helpers.mjs";
2
3
  import { ValidationError } from "../models/validation.model.mjs";
4
+ import { schematicProperties } from "../schematic.mjs";
3
5
  import { isPlainObject } from "@oscarpalmer/atoms/is";
6
+ import { join } from "@oscarpalmer/atoms/string";
4
7
  //#region src/validation/value.validation.ts
5
8
  function validateNamed(property, name, value, validation) {
6
9
  if (!validators[name](value)) return false;
@@ -11,6 +14,7 @@ function validateNamed(property, name, value, validation) {
11
14
  const validator = propertyValidators[index];
12
15
  if (!validator(value)) {
13
16
  validation.push({
17
+ value,
14
18
  key: { ...property.key },
15
19
  message: getInvalidValidatorMessage(property, name, index, length),
16
20
  validator
@@ -20,63 +24,104 @@ function validateNamed(property, name, value, validation) {
20
24
  }
21
25
  return true;
22
26
  }
23
- function validateObject(obj, properties, reporting, validation) {
27
+ function validateObject(obj, properties, options, origin, validation) {
24
28
  if (!isPlainObject(obj)) {
25
- if (reporting.throw && validation == null) throw new ValidationError([{
26
- key: {
27
- full: "",
28
- short: ""
29
- },
30
- message: getInvalidInputMessage(obj)
31
- }]);
32
- return false;
29
+ const key = origin == null ? {
30
+ full: "",
31
+ short: ""
32
+ } : { ...origin.key };
33
+ const information = {
34
+ key,
35
+ message: origin == null ? getInvalidInputMessage(obj) : getInvalidTypeMessage({
36
+ ...origin,
37
+ key
38
+ }, obj),
39
+ value: obj
40
+ };
41
+ if (options.reporting.throw) throw new ValidationError([information]);
42
+ validation?.push(information);
43
+ return options.reporting.none ? false : [information];
44
+ }
45
+ if (options.strict) {
46
+ const objKeys = Object.keys(obj);
47
+ const propertiesKeys = new Set(properties.map((property) => property.key.short));
48
+ const unknownKeys = objKeys.filter((key) => !propertiesKeys.has(key));
49
+ if (unknownKeys.length > 0) {
50
+ const information = {
51
+ key: origin == null ? {
52
+ full: "",
53
+ short: ""
54
+ } : { ...origin.key },
55
+ message: getUnknownKeysMessage(unknownKeys.map((key) => join([origin?.key.full, key], "."))),
56
+ value: obj
57
+ };
58
+ if (options.reporting.throw) throw new ValidationError([information]);
59
+ validation?.push(information);
60
+ return options.reporting.none ? false : [information];
61
+ }
33
62
  }
63
+ const allInformation = [];
34
64
  const propertiesLength = properties.length;
35
65
  outer: for (let propertyIndex = 0; propertyIndex < propertiesLength; propertyIndex += 1) {
36
- const property = properties[propertyIndex];
66
+ let property = properties[propertyIndex];
67
+ property = {
68
+ ...property,
69
+ key: {
70
+ full: join([origin?.key.full, property.key.short], "."),
71
+ short: property.key.short
72
+ }
73
+ };
37
74
  const { key, required, types } = property;
38
75
  const value = obj[key.short];
39
76
  if (value === void 0 && required) {
40
77
  const information = {
78
+ value,
41
79
  key: { ...key },
42
80
  message: getInvalidMissingMessage(property)
43
81
  };
44
- if (reporting.throw && validation == null) throw new ValidationError([information]);
82
+ if (options.reporting.throw && validation == null) throw new ValidationError([information]);
45
83
  if (validation != null) validation.push(information);
46
- return false;
84
+ if (options.reporting.all) {
85
+ allInformation.push(information);
86
+ continue;
87
+ }
88
+ return options.reporting.none ? false : [information];
47
89
  }
48
90
  const typesLength = types.length;
49
91
  const information = [];
50
92
  for (let typeIndex = 0; typeIndex < typesLength; typeIndex += 1) {
51
93
  const type = types[typeIndex];
52
- if (validateValue(type, property, value, reporting, information)) continue outer;
94
+ if (validateValue(type, property, value, options, information)) continue outer;
53
95
  }
54
- if (reporting.throw && validation == null) throw new ValidationError(information.length === 0 ? [{
96
+ if (information.length === 0) information.push({
97
+ value,
55
98
  key: { ...key },
56
99
  message: getInvalidTypeMessage(property, value)
57
- }] : information);
100
+ });
101
+ if (options.reporting.throw && validation == null) throw new ValidationError(information);
58
102
  validation?.push(...information);
59
- return false;
103
+ if (options.reporting.all) {
104
+ allInformation.push(...information);
105
+ continue;
106
+ }
107
+ return options.reporting.none ? false : information;
60
108
  }
61
- return true;
109
+ return options.reporting.none || allInformation.length === 0 ? true : allInformation;
62
110
  }
63
- function validateValue(type, property, value, reporting, validation) {
64
- let result;
111
+ function validateSchematic(property, schematic, value, options, validation) {
112
+ const result = validateObject(value, schematicProperties.get(schematic), options, property, validation);
113
+ return typeof result === "boolean" ? result : result.length === 0;
114
+ }
115
+ function validateValue(type, property, value, options, validation) {
65
116
  switch (true) {
66
- case typeof type === "function":
67
- result = type(value);
68
- break;
69
- case Array.isArray(type):
70
- result = validateObject(value, type, reporting, validation);
71
- break;
72
- case isSchematic(type):
73
- result = type.is(value, reporting);
74
- break;
75
- default:
76
- result = validateNamed(property, type, value, validation);
77
- break;
117
+ case typeof type === "function": return type(value);
118
+ case Array.isArray(type): {
119
+ const validated = validateObject(value, type, options, property, validation);
120
+ return typeof validated === "boolean" ? validated : false;
121
+ }
122
+ case isSchematic(type): return validateSchematic(property, type, value, options, validation);
123
+ default: return validateNamed(property, type, value, validation);
78
124
  }
79
- return result;
80
125
  }
81
126
  const validators = {
82
127
  array: Array.isArray,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@oscarpalmer/jhunal",
3
- "version": "0.18.0",
3
+ "version": "0.20.0",
4
4
  "description": "Flies free beneath the glistening moons…",
5
5
  "keywords": [
6
6
  "schema",
package/src/constants.ts CHANGED
@@ -1,6 +1,20 @@
1
1
  import type {ValueName} from './models/misc.model';
2
2
  import type {ReportingType} from './models/validation.model';
3
3
 
4
+ // #region Grammar
5
+
6
+ export const COMMA = ', ';
7
+
8
+ export const CONJUNCTION_OR = ' or ';
9
+
10
+ export const CONJUNCTION_OR_COMMA = ', or ';
11
+
12
+ export const CONJUNCTION_AND = ' and ';
13
+
14
+ export const CONJUNCTION_AND_COMMA = ', and ';
15
+
16
+ // #endregion
17
+
4
18
  // #region Misc.
5
19
 
6
20
  export const MESSAGE_CONSTRUCTOR = 'Expected a constructor function';
@@ -42,6 +56,8 @@ export const VALIDATION_MESSAGE_INVALID_VALUE =
42
56
 
43
57
  export const VALIDATION_MESSAGE_INVALID_VALUE_SUFFIX = ' at index <>';
44
58
 
59
+ export const VALIDATION_MESSAGE_UNKNOWN_KEYS = 'Found keys that are not defined in the schema: <>';
60
+
45
61
  // #endregion
46
62
 
47
63
  // #region Reporting
package/src/helpers.ts CHANGED
@@ -1,6 +1,11 @@
1
1
  import {isConstructor, isPlainObject} from '@oscarpalmer/atoms/is';
2
2
  import type {Constructor} from '@oscarpalmer/atoms/models';
3
3
  import {
4
+ COMMA,
5
+ CONJUNCTION_AND,
6
+ CONJUNCTION_AND_COMMA,
7
+ CONJUNCTION_OR,
8
+ CONJUNCTION_OR_COMMA,
4
9
  MESSAGE_CONSTRUCTOR,
5
10
  NAME_SCHEMATIC,
6
11
  PROPERTY_SCHEMATIC,
@@ -19,6 +24,7 @@ import {
19
24
  VALIDATION_MESSAGE_INVALID_TYPE,
20
25
  VALIDATION_MESSAGE_INVALID_VALUE,
21
26
  VALIDATION_MESSAGE_INVALID_VALUE_SUFFIX,
27
+ VALIDATION_MESSAGE_UNKNOWN_KEYS,
22
28
  } from './constants';
23
29
  import type {ValueName} from './models/misc.model';
24
30
  import type {
@@ -73,6 +79,29 @@ export function getInvalidValidatorMessage(
73
79
  return message;
74
80
  }
75
81
 
82
+ export function getOptions(input: unknown) {
83
+ if (typeof input === 'boolean') {
84
+ return {
85
+ reporting: getReporting(REPORTING_NONE),
86
+ strict: input,
87
+ };
88
+ }
89
+
90
+ if (REPORTING_TYPES.has(input as ReportingType)) {
91
+ return {
92
+ reporting: getReporting(input as ReportingType),
93
+ strict: false,
94
+ };
95
+ }
96
+
97
+ const options = isPlainObject(input) ? input : {};
98
+
99
+ return {
100
+ reporting: getReporting(options.errors),
101
+ strict: typeof options.strict === 'boolean' ? options.strict : false,
102
+ };
103
+ }
104
+
76
105
  function getPropertyType(original: ValidatedPropertyType): string {
77
106
  if (typeof original === 'function') {
78
107
  return 'a validated value';
@@ -95,6 +124,7 @@ export function getReporting(value: unknown): ReportingInformation {
95
124
  : REPORTING_NONE;
96
125
 
97
126
  return {
127
+ type,
98
128
  [REPORTING_ALL]: type === REPORTING_ALL,
99
129
  [REPORTING_FIRST]: type === REPORTING_FIRST,
100
130
  [REPORTING_NONE]: type === REPORTING_NONE,
@@ -102,6 +132,10 @@ export function getReporting(value: unknown): ReportingInformation {
102
132
  } as ReportingInformation;
103
133
  }
104
134
 
135
+ export function getUnknownKeysMessage(keys: string[]): string {
136
+ return VALIDATION_MESSAGE_UNKNOWN_KEYS.replace(TEMPLATE_PATTERN, renderKeys(keys));
137
+ }
138
+
105
139
  function getValueType(value: unknown): string {
106
140
  const valueType = typeof value;
107
141
 
@@ -161,34 +195,46 @@ export function isSchematic(value: unknown): value is Schematic<never> {
161
195
  );
162
196
  }
163
197
 
164
- function renderTypes(types: ValidatedPropertyType[]): string {
165
- const unique = new Set<string>();
166
- const parts: string[] = [];
167
-
168
- for (let index = 0; index < types.length; index += 1) {
169
- const rendered = getPropertyType(types[index]);
198
+ function renderKeys(keys: string[]): string {
199
+ return renderParts(keys.map(key => `'${key}'`), CONJUNCTION_AND, CONJUNCTION_AND_COMMA);
200
+ }
170
201
 
171
- if (unique.has(rendered)) {
172
- continue;
173
- }
202
+ function renderParts(parts: string[], delimiterShort: string, delimiterLong: string): string {
203
+ const {length} = parts;
174
204
 
175
- unique.add(rendered);
176
- parts.push(rendered);
205
+ if (length === 1) {
206
+ return parts[0];
177
207
  }
178
208
 
179
- const {length} = parts;
180
-
181
209
  let rendered = '';
182
210
 
183
211
  for (let index = 0; index < length; index += 1) {
184
212
  rendered += parts[index];
185
213
 
186
214
  if (index < length - 2) {
187
- rendered += ', ';
215
+ rendered += COMMA;
188
216
  } else if (index === length - 2) {
189
- rendered += parts.length > 2 ? ', or ' : ' or ';
217
+ rendered += parts.length > 2 ? delimiterLong : delimiterShort;
190
218
  }
191
219
  }
192
220
 
193
221
  return rendered;
194
222
  }
223
+
224
+ function renderTypes(types: ValidatedPropertyType[]): string {
225
+ const unique = new Set<string>();
226
+ const parts: string[] = [];
227
+
228
+ for (let index = 0; index < types.length; index += 1) {
229
+ const rendered = getPropertyType(types[index]);
230
+
231
+ if (unique.has(rendered)) {
232
+ continue;
233
+ }
234
+
235
+ unique.add(rendered);
236
+ parts.push(rendered);
237
+ }
238
+
239
+ return renderParts(parts, CONJUNCTION_OR, CONJUNCTION_OR_COMMA);
240
+ }
@@ -6,7 +6,7 @@ import type {PlainSchema, Schema, SchemaProperty} from './schema.plain.model';
6
6
  /**
7
7
  * Infers the TypeScript type from a {@link Schema} definition
8
8
  *
9
- * @template Model - Schema to infer types from
9
+ * @template Model Schema to infer types from
10
10
  *
11
11
  * @example
12
12
  * ```ts
@@ -38,20 +38,20 @@ export type InferOptionalKeys<Model extends Schema> = keyof {
38
38
  };
39
39
 
40
40
  /**
41
- * Infers the TypeScript type of a {@link SchemaProperty}'s `$type` field, unwrapping arrays to infer their item type
41
+ * Infers the TypeScript type from a {@link SchemaProperty}'s `$type` field
42
42
  *
43
- * @template Value - `$type` value _(single or array)_
43
+ * @template Value `$type` value _(single or array)_
44
44
  */
45
45
  export type InferPropertyType<Value> = Value extends (infer Item)[]
46
46
  ? InferPropertyValue<Item>
47
47
  : InferPropertyValue<Value>;
48
48
 
49
49
  /**
50
- * Maps a single type definition to its TypeScript equivalent
50
+ * Maps a single `$type` definition to its TypeScript equivalent
51
51
  *
52
- * Resolves, in order: {@link Constructor} instances, {@link Schematic} models, {@link ValueName} strings, and nested {@link Schema} objects
52
+ * Resolves, in order: {@link Constructor} instances, {@link Schematic} models, {@link ValueName} strings, and nested {@link PlainSchema} objects
53
53
  *
54
- * @template Value - single type definition
54
+ * @template Value single type definition
55
55
  */
56
56
  export type InferPropertyValue<Value> =
57
57
  Value extends Constructor<infer Instance>
@@ -67,27 +67,27 @@ export type InferPropertyValue<Value> =
67
67
  /**
68
68
  * Extracts keys from a {@link Schema} whose entries are required _(i.e., `$required` is not `false`)_
69
69
  *
70
- * @template Model - Schema to extract required keys from
70
+ * @template Model Schema to extract required keys from
71
71
  */
72
72
  export type InferRequiredKeys<Model extends Schema> = keyof {
73
73
  [Key in keyof Model as IsOptionalProperty<Model[Key]> extends true ? never : Key]: never;
74
74
  };
75
75
 
76
76
  /**
77
- * Infers the type for a top-level {@link Schema} entry, unwrapping arrays to infer their item type
77
+ * Infers the TypeScript type from a top-level {@link Schema} entry
78
78
  *
79
- * @template Value - Schema entry value _(single or array)_
79
+ * @template Value Schema entry value _(single or array)_
80
80
  */
81
81
  export type InferSchemaEntry<Value> = Value extends (infer Item)[]
82
82
  ? InferSchemaEntryValue<Item>
83
83
  : InferSchemaEntryValue<Value>;
84
84
 
85
85
  /**
86
- * Resolves a single schema entry to its TypeScript type
86
+ * Maps a single top-level schema entry to its TypeScript type
87
87
  *
88
- * Handles, in order: {@link Constructor} instances, {@link Schematic} models, {@link SchemaProperty} objects, {@link NestedSchema} objects, {@link ValueName} strings, and plain {@link Schema} objects
88
+ * Resolves, in order: {@link Constructor} instances, {@link Schematic} models, {@link SchemaProperty} objects, {@link PlainSchema} objects, and {@link ValueName} strings
89
89
  *
90
- * @template Value - single schema entry
90
+ * @template Value single schema entry
91
91
  */
92
92
  export type InferSchemaEntryValue<Value> =
93
93
  Value extends Constructor<infer Instance>