@oscarpalmer/jhunal 0.13.0 → 0.15.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.
@@ -2,11 +2,10 @@ import {isConstructor, isPlainObject} from '@oscarpalmer/atoms/is';
2
2
  import type {PlainObject} from '@oscarpalmer/atoms/models';
3
3
  import {join} from '@oscarpalmer/atoms/string/misc';
4
4
  import {
5
- EXPRESSION_INDEX,
6
5
  EXPRESSION_PROPERTY,
7
6
  MESSAGE_SCHEMA_INVALID_EMPTY,
8
- MESSAGE_SCHEMA_INVALID_PROPERTY_REQUIRED_DISALLOWED,
9
- MESSAGE_SCHEMA_INVALID_PROPERTY_REQUIRED_TYPE,
7
+ MESSAGE_SCHEMA_INVALID_PROPERTY_DISALLOWED,
8
+ MESSAGE_SCHEMA_INVALID_PROPERTY_REQUIRED,
10
9
  MESSAGE_SCHEMA_INVALID_PROPERTY_TYPE,
11
10
  MESSAGE_VALIDATOR_INVALID_KEY,
12
11
  MESSAGE_VALIDATOR_INVALID_TYPE,
@@ -15,7 +14,10 @@ import {
15
14
  PROPERTY_TYPE,
16
15
  PROPERTY_VALIDATORS,
17
16
  TEMPLATE_PATTERN,
17
+ TEMPLATE_PATTERN_KEY,
18
+ TEMPLATE_PATTERN_PROPERTY,
18
19
  TYPE_ALL,
20
+ TYPE_OBJECT,
19
21
  TYPE_UNDEFINED,
20
22
  VALIDATABLE_TYPES,
21
23
  } from '../constants';
@@ -28,19 +30,40 @@ import {
28
30
  type ValueName,
29
31
  } from '../models';
30
32
 
33
+ function getDisallowedProperty(obj: PlainObject): string | undefined {
34
+ if (PROPERTY_REQUIRED in obj) {
35
+ return PROPERTY_REQUIRED;
36
+ }
37
+
38
+ if (PROPERTY_TYPE in obj) {
39
+ return PROPERTY_TYPE;
40
+ }
41
+
42
+ if (PROPERTY_VALIDATORS in obj) {
43
+ return PROPERTY_VALIDATORS;
44
+ }
45
+ }
46
+
31
47
  export function getProperties(
32
48
  original: PlainObject,
33
49
  prefix?: string,
34
- fromTypes?: boolean,
50
+ fromType?: boolean,
35
51
  ): ValidatedProperty[] {
36
52
  if (Object.keys(original).length === 0) {
37
53
  throw new SchematicError(MESSAGE_SCHEMA_INVALID_EMPTY);
38
54
  }
39
55
 
40
- if (PROPERTY_REQUIRED in original && (fromTypes ?? false) && prefix != null) {
41
- throw new SchematicError(
42
- MESSAGE_SCHEMA_INVALID_PROPERTY_REQUIRED_DISALLOWED.replace(TEMPLATE_PATTERN, prefix),
43
- );
56
+ if (fromType ?? false) {
57
+ const property = getDisallowedProperty(original);
58
+
59
+ if (property != null) {
60
+ throw new SchematicError(
61
+ MESSAGE_SCHEMA_INVALID_PROPERTY_DISALLOWED.replace(TEMPLATE_PATTERN_KEY, prefix!).replace(
62
+ TEMPLATE_PATTERN_PROPERTY,
63
+ property,
64
+ ),
65
+ );
66
+ }
44
67
  }
45
68
 
46
69
  const keys = Object.keys(original);
@@ -51,7 +74,7 @@ export function getProperties(
51
74
  for (let keyIndex = 0; keyIndex < keysLength; keyIndex += 1) {
52
75
  const key = keys[keyIndex];
53
76
 
54
- if (EXPRESSION_INDEX.test(key) || EXPRESSION_PROPERTY.test(key)) {
77
+ if (EXPRESSION_PROPERTY.test(key)) {
55
78
  continue;
56
79
  }
57
80
 
@@ -67,9 +90,9 @@ export function getProperties(
67
90
  validators = getValidators(value[PROPERTY_VALIDATORS]);
68
91
 
69
92
  if (PROPERTY_TYPE in value) {
70
- types.push('object', ...getTypes(key, value[PROPERTY_TYPE], prefix, true));
93
+ types.push(TYPE_OBJECT, ...getTypes(key, value[PROPERTY_TYPE], prefix, true));
71
94
  } else {
72
- types.push('object', ...getTypes(key, value, prefix));
95
+ types.push(TYPE_OBJECT, ...getTypes(key, value, prefix));
73
96
  }
74
97
  } else {
75
98
  types.push(...getTypes(key, value, prefix));
@@ -97,7 +120,7 @@ function getRequired(key: string, obj: PlainObject): boolean | undefined {
97
120
 
98
121
  if (typeof obj[PROPERTY_REQUIRED] !== 'boolean') {
99
122
  throw new SchematicError(
100
- MESSAGE_SCHEMA_INVALID_PROPERTY_REQUIRED_TYPE.replace(TEMPLATE_PATTERN, key),
123
+ MESSAGE_SCHEMA_INVALID_PROPERTY_REQUIRED.replace(TEMPLATE_PATTERN, key),
101
124
  );
102
125
  }
103
126
 
@@ -108,7 +131,7 @@ function getTypes(
108
131
  key: string,
109
132
  original: unknown,
110
133
  prefix?: string,
111
- fromTypes?: boolean,
134
+ fromType?: boolean,
112
135
  ): ValidatedPropertyType[] {
113
136
  const array = Array.isArray(original) ? original : [original];
114
137
  const {length} = array;
@@ -124,7 +147,7 @@ function getTypes(
124
147
  break;
125
148
 
126
149
  case isPlainObject(value):
127
- types.push(...getProperties(value, join([prefix, key], '.'), fromTypes));
150
+ types.push(...getProperties(value, join([prefix, key], '.'), fromType));
128
151
  break;
129
152
 
130
153
  case isSchematic(value):
@@ -1,12 +1,9 @@
1
1
  export declare const ERROR_NAME = "SchematicError";
2
- export declare const EXPRESSION_INDEX: RegExp;
3
- export declare const EXPRESSION_KEY_PREFIX: RegExp;
4
- export declare const EXPRESSION_KEY_VALUE: RegExp;
5
2
  export declare const EXPRESSION_PROPERTY: RegExp;
6
3
  export declare const MESSAGE_CONSTRUCTOR = "Expected a constructor function";
7
4
  export declare const MESSAGE_SCHEMA_INVALID_EMPTY = "Schema must have at least one property";
8
- export declare const MESSAGE_SCHEMA_INVALID_PROPERTY_REQUIRED_DISALLOWED = "'<>.$required' property is not allowed for schemas in $type";
9
- export declare const MESSAGE_SCHEMA_INVALID_PROPERTY_REQUIRED_TYPE = "'<>.$required' property must be a boolean";
5
+ export declare const MESSAGE_SCHEMA_INVALID_PROPERTY_DISALLOWED = "'<key>.<property>' property is not allowed for schemas in $type";
6
+ export declare const MESSAGE_SCHEMA_INVALID_PROPERTY_REQUIRED = "'<>.$required' property must be a boolean";
10
7
  export declare const MESSAGE_SCHEMA_INVALID_PROPERTY_TYPE = "'<>' property must be of a valid type";
11
8
  export declare const MESSAGE_SCHEMA_INVALID_TYPE = "Schema must be an object";
12
9
  export declare const MESSAGE_VALIDATOR_INVALID_KEY = "Validator '<>' does not exist";
@@ -17,6 +14,8 @@ export declare const PROPERTY_TYPE = "$type";
17
14
  export declare const PROPERTY_VALIDATORS = "$validators";
18
15
  export declare const SCHEMATIC_NAME = "$schematic";
19
16
  export declare const TEMPLATE_PATTERN = "<>";
17
+ export declare const TEMPLATE_PATTERN_KEY = "<key>";
18
+ export declare const TEMPLATE_PATTERN_PROPERTY = "<property>";
20
19
  export declare const TYPE_OBJECT = "object";
21
20
  export declare const TYPE_UNDEFINED = "undefined";
22
21
  export declare const VALIDATABLE_TYPES: Set<keyof import("./models").Values>;
@@ -1,4 +1,15 @@
1
- import type { Constructor } from './models';
1
+ import type { Constructor } from '@oscarpalmer/atoms/models';
2
2
  import type { Schematic } from './schematic';
3
+ /**
4
+ * Creates a validator function for a given constructor
5
+ * @param constructor - Constructor to check against
6
+ * @throws Will throw a `TypeError` if the provided argument is not a valid constructor
7
+ * @returns Validator function that checks if a value is an instance of the constructor
8
+ */
3
9
  export declare function instanceOf<Instance>(constructor: Constructor<Instance>): (value: unknown) => value is Instance;
10
+ /**
11
+ * Is the value a schematic?
12
+ * @param value Value to check
13
+ * @returns `true` if the value is a schematic, `false` otherwise
14
+ */
4
15
  export declare function isSchematic(value: unknown): value is Schematic<never>;
package/types/index.d.ts CHANGED
@@ -1,3 +1,3 @@
1
- export { instanceOf } from './helpers';
1
+ export { instanceOf, isSchematic } from './helpers';
2
2
  export { SchematicError, type Schema, type TypedSchema } from './models';
3
3
  export { schematic, type Schematic } from './schematic';
package/types/models.d.ts CHANGED
@@ -1,82 +1,338 @@
1
- import type { GenericCallback, PlainObject, Simplify } from '@oscarpalmer/atoms/models';
1
+ import type { Constructor, GenericCallback, PlainObject, Simplify } from '@oscarpalmer/atoms/models';
2
2
  import type { Schematic } from './schematic';
3
- export type Constructor<Instance = any> = new (...args: any[]) => Instance;
3
+ /**
4
+ * Removes duplicate types from a tuple, preserving first occurrence order
5
+ *
6
+ * @template Value - Tuple to deduplicate
7
+ * @template Seen - Accumulator for already-seen types _(internal)_
8
+ *
9
+ * @example
10
+ * ```ts
11
+ * // DeduplicateTuple<['string', 'number', 'string']>
12
+ * // => ['string', 'number']
13
+ * ```
14
+ */
4
15
  type DeduplicateTuple<Value extends unknown[], Seen extends unknown[] = []> = Value extends [
5
16
  infer Head,
6
17
  ...infer Tail
7
18
  ] ? Head extends Seen[number] ? DeduplicateTuple<Tail, Seen> : DeduplicateTuple<Tail, [...Seen, Head]> : Seen;
19
+ /**
20
+ * Recursively extracts {@link ValueName} strings from a type, unwrapping arrays and readonly arrays
21
+ *
22
+ * @template Value - Type to extract value names from
23
+ *
24
+ * @example
25
+ * ```ts
26
+ * // ExtractValueNames<'string'> => 'string'
27
+ * // ExtractValueNames<['string', 'number']> => 'string' | 'number'
28
+ * ```
29
+ */
8
30
  type ExtractValueNames<Value> = Value extends ValueName ? Value : Value extends (infer Item)[] ? ExtractValueNames<Item> : Value extends readonly (infer Item)[] ? ExtractValueNames<Item> : never;
9
31
  /**
10
- * Infer the TypeScript type from a schema definition
32
+ * Infers the TypeScript type from a {@link Schema} definition
33
+ *
34
+ * @template Model - Schema to infer types from
35
+ *
36
+ * @example
37
+ * ```ts
38
+ * const userSchema = {
39
+ * name: 'string',
40
+ * age: 'number',
41
+ * address: { $required: false, $type: 'string' },
42
+ * } satisfies Schema;
43
+ *
44
+ * type User = Infer<typeof userSchema>;
45
+ * // { name: string; age: number; address?: string }
46
+ * ```
11
47
  */
12
48
  export type Infer<Model extends Schema> = Simplify<{
13
49
  [Key in InferRequiredKeys<Model>]: InferSchemaEntry<Model[Key]>;
14
50
  } & {
15
51
  [Key in InferOptionalKeys<Model>]?: InferSchemaEntry<Model[Key]>;
16
52
  }>;
53
+ /**
54
+ * Extracts keys from a {@link Schema} whose entries are optional _(i.e., `$required` is `false`)_
55
+ *
56
+ * @template Model - {@link Schema} to extract optional keys from
57
+ */
17
58
  type InferOptionalKeys<Model extends Schema> = keyof {
18
59
  [Key in keyof Model as IsOptionalProperty<Model[Key]> extends true ? Key : never]: never;
19
60
  };
61
+ /**
62
+ * Infers the TypeScript type of a {@link SchemaProperty}'s `$type` field, unwrapping arrays to infer their item type
63
+ *
64
+ * @template Value - `$type` value _(single or array)_
65
+ */
20
66
  type InferPropertyType<Value> = Value extends (infer Item)[] ? InferPropertyValue<Item> : InferPropertyValue<Value>;
67
+ /**
68
+ * Maps a single type definition to its TypeScript equivalent
69
+ *
70
+ * Resolves, in order: {@link Constructor} instances, {@link Schematic} models, {@link ValueName} strings, and nested {@link Schema} objects
71
+ *
72
+ * @template Value - single type definition
73
+ */
21
74
  type InferPropertyValue<Value> = Value extends Constructor<infer Instance> ? Instance : Value extends Schematic<infer Model> ? Model : Value extends ValueName ? Values[Value & ValueName] : Value extends Schema ? Infer<Value> : never;
75
+ /**
76
+ * Extracts keys from a {@link Schema} whose entries are required _(i.e., `$required` is not `false`)_
77
+ *
78
+ * @template Model - Schema to extract required keys from
79
+ */
22
80
  type InferRequiredKeys<Model extends Schema> = keyof {
23
81
  [Key in keyof Model as IsOptionalProperty<Model[Key]> extends true ? never : Key]: never;
24
82
  };
83
+ /**
84
+ * Infers the type for a top-level {@link Schema} entry, unwrapping arrays to infer their item type
85
+ *
86
+ * @template Value - Schema entry value _(single or array)_
87
+ */
25
88
  type InferSchemaEntry<Value> = Value extends (infer Item)[] ? InferSchemaEntryValue<Item> : InferSchemaEntryValue<Value>;
89
+ /**
90
+ * Resolves a single schema entry to its TypeScript type
91
+ *
92
+ * Handles, in order: {@link Constructor} instances, {@link Schematic} models, {@link SchemaProperty} objects, {@link NestedSchema} objects, {@link ValueName} strings, and plain {@link Schema} objects
93
+ *
94
+ * @template Value - single schema entry
95
+ */
26
96
  type InferSchemaEntryValue<Value> = Value extends Constructor<infer Instance> ? Instance : Value extends Schematic<infer Model> ? Model : Value extends SchemaProperty ? InferPropertyType<Value['$type']> : Value extends NestedSchema ? Infer<Omit<Value, '$required'>> : Value extends ValueName ? Values[Value & ValueName] : Value extends Schema ? Infer<Value> : never;
97
+ /**
98
+ * Determines whether a schema entry is optional
99
+ *
100
+ * Returns `true` if the entry is a {@link SchemaProperty} or {@link NestedSchema} with `$required` set to `false`; otherwise returns `false`
101
+ *
102
+ * @template Value - Schema entry to check
103
+ */
27
104
  type IsOptionalProperty<Value> = Value extends SchemaProperty ? Value['$required'] extends false ? true : false : Value extends {
28
105
  $required?: boolean;
29
106
  } ? Value extends {
30
107
  $required: false;
31
108
  } ? true : false : false;
109
+ /**
110
+ * Extracts the last member from a union type by leveraging intersection of function return types
111
+ *
112
+ * @template Value - Union type
113
+ */
32
114
  type LastOfUnion<Value> = UnionToIntersection<Value extends unknown ? () => Value : never> extends () => infer Item ? Item : never;
115
+ /**
116
+ * Maps each element of a tuple through {@link ToValueType}
117
+ *
118
+ * @template Value - Tuple of types to map
119
+ */
33
120
  type MapToValueTypes<Value extends unknown[]> = Value extends [infer Head, ...infer Tail] ? [ToValueType<Head>, ...MapToValueTypes<Tail>] : [];
121
+ /**
122
+ * Maps each element of a tuple through {@link ToSchemaPropertyTypeEach}
123
+ *
124
+ * @template Value - Tuple of types to map
125
+ */
34
126
  type MapToSchemaPropertyTypes<Value extends unknown[]> = Value extends [infer Head, ...infer Tail] ? [ToSchemaPropertyTypeEach<Head>, ...MapToSchemaPropertyTypes<Tail>] : [];
35
127
  /**
36
- * A nested schema with optional requirement flag
128
+ * A nested schema definition that may include a `$required` flag alongside arbitrary string-keyed properties
129
+ *
130
+ * @example
131
+ * ```ts
132
+ * const address: NestedSchema = {
133
+ * $required: false,
134
+ * street: 'string',
135
+ * city: 'string',
136
+ * };
137
+ * ```
37
138
  */
38
139
  export type NestedSchema = {
140
+ /**
141
+ * Whether the nested schema is required (defaults to `true`)
142
+ */
39
143
  $required?: boolean;
40
- [key: string]: any;
41
- };
144
+ } & Schema;
145
+ /**
146
+ * Extracts keys from an object type that are optional
147
+ *
148
+ * @template Value - Object type to inspect
149
+ */
42
150
  type OptionalKeys<Value> = {
43
151
  [Key in keyof Value]-?: {} extends Pick<Value, Key> ? Key : never;
44
152
  }[keyof Value];
153
+ /**
154
+ * A generic schema allowing {@link NestedSchema}, {@link SchemaEntry}, or arrays of {@link SchemaEntry} as values
155
+ */
156
+ type PlainSchema = {
157
+ [key: string]: NestedSchema | SchemaEntry | SchemaEntry[];
158
+ };
159
+ /**
160
+ * A map of optional validator functions keyed by {@link ValueName}, used to add custom validation to {@link SchemaProperty} definitions
161
+ *
162
+ * Each key may hold a single validator or an array of validators that receive the typed value
163
+ *
164
+ * @template Value - `$type` value(s) to derive validator keys from
165
+ *
166
+ * @example
167
+ * ```ts
168
+ * const validators: PropertyValidators<'string'> = {
169
+ * string: (value) => value.length > 0,
170
+ * };
171
+ * ```
172
+ */
45
173
  type PropertyValidators<Value> = {
46
174
  [Key in ExtractValueNames<Value>]?: ((value: Values[Key]) => boolean) | Array<(value: Values[Key]) => boolean>;
47
175
  };
176
+ /**
177
+ * Extracts keys from an object type that are required _(i.e., not optional)_
178
+ *
179
+ * @template Value - Object type to inspect
180
+ */
48
181
  type RequiredKeys<Value> = Exclude<keyof Value, OptionalKeys<Value>>;
49
182
  /**
50
183
  * A schema for validating objects
184
+ *
185
+ * @example
186
+ * ```ts
187
+ * const schema: Schema = {
188
+ * name: 'string',
189
+ * age: 'number',
190
+ * tags: ['string', 'number'],
191
+ * };
192
+ * ```
51
193
  */
52
194
  export type Schema = SchemaIndex;
195
+ /**
196
+ * A union of all valid types for a single schema entry
197
+ *
198
+ * Can be a {@link Constructor}, nested {@link Schema}, {@link SchemaProperty}, {@link Schematic}, {@link ValueName} string, or a custom validator function
199
+ */
53
200
  type SchemaEntry = Constructor | Schema | SchemaProperty | Schematic<unknown> | ValueName | ((value: unknown) => boolean);
201
+ /**
202
+ * Index signature interface backing {@link Schema}, allowing string-keyed entries of {@link NestedSchema}, {@link SchemaEntry}, or arrays of {@link SchemaEntry}
203
+ */
54
204
  interface SchemaIndex {
55
205
  [key: string]: NestedSchema | SchemaEntry | SchemaEntry[];
56
206
  }
57
207
  /**
58
- * A property definition with explicit type(s) and optional requirement flag
208
+ * A property definition with explicit type(s), an optional requirement flag, and optional validators
209
+ *
210
+ * @example
211
+ * ```ts
212
+ * const prop: SchemaProperty = {
213
+ * $required: false,
214
+ * $type: ['string', 'number'],
215
+ * $validators: {
216
+ * string: (v) => v.length > 0,
217
+ * number: (v) => v > 0,
218
+ * },
219
+ * };
220
+ * ```
59
221
  */
60
222
  export type SchemaProperty = {
223
+ /**
224
+ * Whether the property is required _(defaults to `true`)_
225
+ */
61
226
  $required?: boolean;
227
+ /**
228
+ * The type(s) the property value must match; a single {@link SchemaPropertyType} or an array
229
+ */
62
230
  $type: SchemaPropertyType | SchemaPropertyType[];
231
+ /**
232
+ * Optional validators keyed by {@link ValueName}, applied during validation
233
+ */
63
234
  $validators?: PropertyValidators<SchemaPropertyType | SchemaPropertyType[]>;
64
235
  };
65
- type SchemaPropertyType = Constructor | Schema | Schematic<unknown> | ValueName | ((value: unknown) => boolean);
236
+ /**
237
+ * A union of valid types for a {@link SchemaProperty}'s `$type` field
238
+ *
239
+ * Can be a {@link Constructor}, {@link PlainSchema}, {@link Schematic}, {@link ValueName} string, or a custom validator function
240
+ */
241
+ type SchemaPropertyType = Constructor | PlainSchema | Schematic<unknown> | ValueName | ((value: unknown) => boolean);
242
+ /**
243
+ * A custom error class for schema validation failures, with its `name` set to {@link ERROR_NAME}
244
+ *
245
+ * @example
246
+ * ```ts
247
+ * throw new SchematicError('Expected a string, received a number');
248
+ * ```
249
+ */
66
250
  export declare class SchematicError extends Error {
67
251
  constructor(message: string);
68
252
  }
253
+ /**
254
+ * Converts a type into its corresponding {@link SchemaPropertyType}-representation
255
+ *
256
+ * Deduplicates and unwraps single-element tuples via {@link UnwrapSingle}
257
+ *
258
+ * @template Value - type to convert
259
+ */
69
260
  type ToSchemaPropertyType<Value> = UnwrapSingle<DeduplicateTuple<MapToSchemaPropertyTypes<UnionToTuple<Value>>>>;
261
+ /**
262
+ * Converts a single type to its schema property equivalent
263
+ *
264
+ * {@link NestedSchema} values have `$required` stripped, plain objects become {@link TypedSchema}, and primitives go through {@link ToValueType}
265
+ *
266
+ * @template Value - type to convert
267
+ */
70
268
  type ToSchemaPropertyTypeEach<Value> = Value extends NestedSchema ? Omit<Value, '$required'> : Value extends PlainObject ? TypedSchema<Value> : ToValueType<Value>;
269
+ /**
270
+ * Converts a type into its corresponding {@link ValueName}-representation
271
+ *
272
+ * Deduplicates and unwraps single-element tuples via {@link UnwrapSingle}
273
+ *
274
+ * @template Value - type to convert
275
+ */
71
276
  type ToSchemaType<Value> = UnwrapSingle<DeduplicateTuple<MapToValueTypes<UnionToTuple<Value>>>>;
72
- type ToValueType<Value> = Value extends unknown[] ? 'array' : Value extends bigint ? 'bigint' : Value extends boolean ? 'boolean' : Value extends Date ? 'date' : Value extends Schematic<any> ? Value : Value extends Function ? 'function' : Value extends null ? 'null' : Value extends number ? 'number' : Value extends object ? 'object' | ((value: unknown) => value is Value) : Value extends string ? 'string' : Value extends symbol ? 'symbol' : Value extends undefined ? 'undefined' : (value: unknown) => value is Value;
277
+ /**
278
+ * Maps a type to its {@link ValueName} string equivalent
279
+ *
280
+ * Resolves {@link Schematic} types as-is, then performs a reverse-lookup against {@link Values} _(excluding `'object'`)_ to find a matching key. If no match is found, `object` types resolve to `'object'` or a type-guard function, and all other unrecognised types resolve to a type-guard function
281
+ *
282
+ * @template Value - type to map
283
+ *
284
+ * @example
285
+ * ```ts
286
+ * // ToValueType<string> => 'string'
287
+ * // ToValueType<number[]> => 'array'
288
+ * // ToValueType<Date> => 'date'
289
+ * ```
290
+ */
291
+ type ToValueType<Value> = Value extends Schematic<any> ? Value : {
292
+ [Key in keyof Omit<Values, 'object'>]: Value extends Values[Key] ? Key : never;
293
+ }[keyof Omit<Values, 'object'>] extends infer Match ? [Match] extends [never] ? Value extends object ? 'object' | ((value: unknown) => value is Value) : (value: unknown) => value is Value : Match : never;
294
+ /**
295
+ * Generates all permutations of a tuple type
296
+ *
297
+ * Used by {@link UnwrapSingle} to allow schema types in any order for small tuples _(length ≤ 5)_
298
+ *
299
+ * @template Tuple - Tuple to permute
300
+ * @template Elput - Accumulator for the current permutation _(internal; name is Tuple backwards)_
301
+ *
302
+ * @example
303
+ * ```ts
304
+ * // TuplePermutations<['string', 'number']>
305
+ * // => ['string', 'number'] | ['number', 'string']
306
+ * ```
307
+ */
73
308
  type TuplePermutations<Tuple extends unknown[], Elput extends unknown[] = []> = Tuple['length'] extends 0 ? Elput : {
74
309
  [Key in keyof Tuple]: TuplePermutations<TupleRemoveAt<Tuple, Key & `${number}`>, [
75
310
  ...Elput,
76
311
  Tuple[Key]
77
312
  ]>;
78
313
  }[keyof Tuple & `${number}`];
314
+ /**
315
+ * Removes the element at a given index from a tuple
316
+ *
317
+ * Used internally by {@link TuplePermutations}
318
+ *
319
+ * @template Items - Tuple to remove from
320
+ * @template Item - Stringified index to remove
321
+ * @template Prefix - Accumulator for elements before the target _(internal)_
322
+ */
79
323
  type TupleRemoveAt<Items extends unknown[], Item extends string, Prefix extends unknown[] = []> = Items extends [infer Head, ...infer Tail] ? `${Prefix['length']}` extends Item ? [...Prefix, ...Tail] : TupleRemoveAt<Tail, Item, [...Prefix, Head]> : Prefix;
324
+ /**
325
+ * A typed optional property definition generated by {@link TypedSchema} for optional keys, with `$required` set to `false` and excludes `undefined` from the type
326
+ *
327
+ * @template Value - Property's type _(including `undefined`)_
328
+ *
329
+ * @example
330
+ * ```ts
331
+ * // For `{ name?: string }`, the `name` key produces:
332
+ * // TypedPropertyOptional<string | undefined>
333
+ * // => { $required: false; $type: 'string'; ... }
334
+ * ```
335
+ */
80
336
  export type TypedPropertyOptional<Value> = {
81
337
  /**
82
338
  * The property is not required
@@ -91,6 +347,18 @@ export type TypedPropertyOptional<Value> = {
91
347
  */
92
348
  $validators?: PropertyValidators<ToSchemaPropertyType<Exclude<Value, undefined>>>;
93
349
  };
350
+ /**
351
+ * A typed required property definition generated by {@link TypedSchema} for required keys, with `$required` defaulting to `true`
352
+ *
353
+ * @template Value - Property's type
354
+ *
355
+ * @example
356
+ * ```ts
357
+ * // For `{ name: string }`, the `name` key produces:
358
+ * // TypedPropertyRequired<string>
359
+ * // => { $required?: true; $type: 'string'; ... }
360
+ * ```
361
+ */
94
362
  export type TypedPropertyRequired<Value> = {
95
363
  /**
96
364
  * The property is required _(defaults to `true`)_
@@ -106,38 +374,147 @@ export type TypedPropertyRequired<Value> = {
106
374
  $validators?: PropertyValidators<ToSchemaPropertyType<Value>>;
107
375
  };
108
376
  /**
109
- * Create a schema type constrained to match a TypeScript type
377
+ * Creates a schema type constrained to match a TypeScript type
378
+ *
379
+ * Required keys map to {@link ToSchemaType} or {@link TypedPropertyRequired}; plain object values may also use {@link Schematic}. Optional keys map to {@link TypedPropertyOptional} or, for plain objects, {@link TypedSchemaOptional}
380
+ *
381
+ * @template Model - Object type to generate a schema for
382
+ *
383
+ * @example
384
+ * ```ts
385
+ * type User = { name: string; age: number; bio?: string };
386
+ *
387
+ * const schema: TypedSchema<User> = {
388
+ * name: 'string',
389
+ * age: 'number',
390
+ * bio: { $required: false, $type: 'string' },
391
+ * };
392
+ * ```
110
393
  */
111
394
  export type TypedSchema<Model extends PlainObject> = Simplify<{
112
395
  [Key in RequiredKeys<Model>]: Model[Key] extends PlainObject ? TypedSchemaRequired<Model[Key]> | Schematic<Model[Key]> : ToSchemaType<Model[Key]> | TypedPropertyRequired<Model[Key]>;
113
396
  } & {
114
397
  [Key in OptionalKeys<Model>]: Exclude<Model[Key], undefined> extends PlainObject ? TypedSchemaOptional<Exclude<Model[Key], undefined>> | Schematic<Exclude<Model[Key], undefined>> : TypedPropertyOptional<Model[Key]>;
115
398
  }>;
399
+ /**
400
+ * A {@link TypedSchema} variant for optional nested objects, with `$required` fixed to `false`
401
+ *
402
+ * @template Model - Nested object type
403
+ */
116
404
  type TypedSchemaOptional<Model extends PlainObject> = {
117
405
  $required: false;
118
406
  } & TypedSchema<Model>;
407
+ /**
408
+ * A {@link TypedSchema} variant for required nested objects, with `$required` defaulting to `true`
409
+ *
410
+ * @template Model - Nested object type
411
+ */
119
412
  type TypedSchemaRequired<Model extends PlainObject> = {
120
413
  $required?: true;
121
414
  } & TypedSchema<Model>;
415
+ /**
416
+ * Converts a union type into an intersection
417
+ *
418
+ * Uses the contravariance of function parameter types to collapse a union into an intersection
419
+ *
420
+ * @template Value - Union type to convert
421
+ *
422
+ * @example
423
+ * ```ts
424
+ * // UnionToIntersection<{ a: 1 } | { b: 2 }>
425
+ * // => { a: 1 } & { b: 2 }
426
+ * ```
427
+ */
122
428
  type UnionToIntersection<Value> = (Value extends unknown ? (value: Value) => void : never) extends (value: infer Item) => void ? Item : never;
429
+ /**
430
+ * Converts a union type into an ordered tuple
431
+ *
432
+ * Repeatedly extracts the {@link LastOfUnion} member and prepends it to the accumulator
433
+ *
434
+ * @template Value - Union type to convert
435
+ * @template Items - Accumulator for the resulting tuple _(internal)_
436
+ *
437
+ * @example
438
+ * ```ts
439
+ * // UnionToTuple<'a' | 'b' | 'c'>
440
+ * // => ['a', 'b', 'c']
441
+ * ```
442
+ */
123
443
  type UnionToTuple<Value, Items extends unknown[] = []> = [Value] extends [never] ? Items : UnionToTuple<Exclude<Value, LastOfUnion<Value>>, [LastOfUnion<Value>, ...Items]>;
444
+ /**
445
+ * Unwraps a single-element tuple to its inner type
446
+ *
447
+ * For tuples of length 2–5, returns all {@link TuplePermutations} to allow types in any order. Longer tuples are returned as-is
448
+ *
449
+ * @template Value - Tuple to potentially unwrap
450
+ *
451
+ * @example
452
+ * ```ts
453
+ * // UnwrapSingle<['string']> => 'string'
454
+ * // UnwrapSingle<['string', 'number']> => ['string', 'number'] | ['number', 'string']
455
+ * ```
456
+ */
124
457
  type UnwrapSingle<Value extends unknown[]> = Value extends [infer Only] ? Only : Value['length'] extends 1 | 2 | 3 | 4 | 5 ? TuplePermutations<Value> : Value;
458
+ /**
459
+ * The runtime representation of a parsed schema property, used internally during validation
460
+ *
461
+ * @example
462
+ * ```ts
463
+ * const parsed: ValidatedProperty = {
464
+ * key: 'age',
465
+ * required: true,
466
+ * types: ['number'],
467
+ * validators: { number: [(v) => v > 0] },
468
+ * };
469
+ * ```
470
+ */
125
471
  export type ValidatedProperty = {
472
+ /**
473
+ * The property name in the schema
474
+ */
126
475
  key: string;
476
+ /**
477
+ * Whether the property is required
478
+ */
127
479
  required: boolean;
480
+ /**
481
+ * The allowed types for this property
482
+ */
128
483
  types: ValidatedPropertyType[];
484
+ /**
485
+ * Custom validators grouped by {@link ValueName}
486
+ */
129
487
  validators: ValidatedPropertyValidators;
130
488
  };
489
+ /**
490
+ * A union of valid types for a {@link ValidatedProperty}'s `types` array
491
+ *
492
+ * Can be a callback _(custom validator)_, a {@link Schematic}, a nested {@link ValidatedProperty}, or a {@link ValueName} string
493
+ */
131
494
  export type ValidatedPropertyType = GenericCallback | Schematic<unknown> | ValidatedProperty | ValueName;
495
+ /**
496
+ * A map of validator functions keyed by {@link ValueName}, used at runtime in {@link ValidatedProperty}
497
+ *
498
+ * Each key holds an array of validator functions that receive an `unknown` value and return a `boolean`
499
+ */
132
500
  export type ValidatedPropertyValidators = {
133
501
  [Key in ValueName]?: Array<(value: unknown) => boolean>;
134
502
  };
135
503
  /**
136
- * Valid type name strings
504
+ * Basic value types
137
505
  */
138
506
  export type ValueName = keyof Values;
139
507
  /**
140
- * Map of type names to their TypeScript/validatable equivalents
508
+ * Maps type name strings to their TypeScript equivalents
509
+ *
510
+ * Used by the type system to resolve {@link ValueName} strings into actual types
511
+ *
512
+ * @example
513
+ * ```ts
514
+ * // Values['string'] => string
515
+ * // Values['date'] => Date
516
+ * // Values['null'] => null
517
+ * ```
141
518
  */
142
519
  export type Values = {
143
520
  array: unknown[];