@spudlabs/guardis 0.4.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.
Files changed (106) hide show
  1. package/README.md +901 -0
  2. package/esm/mod.d.ts +55 -0
  3. package/esm/mod.d.ts.map +1 -0
  4. package/esm/mod.js +43 -0
  5. package/esm/package.json +3 -0
  6. package/esm/specs/standard-schema-spec.v1.d.ts +56 -0
  7. package/esm/specs/standard-schema-spec.v1.d.ts.map +1 -0
  8. package/esm/specs/standard-schema-spec.v1.js +1 -0
  9. package/esm/src/batch.d.ts +23 -0
  10. package/esm/src/batch.d.ts.map +1 -0
  11. package/esm/src/batch.js +23 -0
  12. package/esm/src/brand.d.ts +62 -0
  13. package/esm/src/brand.d.ts.map +1 -0
  14. package/esm/src/brand.js +9 -0
  15. package/esm/src/context.d.ts +19 -0
  16. package/esm/src/context.d.ts.map +1 -0
  17. package/esm/src/context.js +41 -0
  18. package/esm/src/extend.d.ts +23 -0
  19. package/esm/src/extend.d.ts.map +1 -0
  20. package/esm/src/extend.js +9 -0
  21. package/esm/src/guard.d.ts +288 -0
  22. package/esm/src/guard.d.ts.map +1 -0
  23. package/esm/src/guard.js +631 -0
  24. package/esm/src/helpers/http.helpers.d.ts +3 -0
  25. package/esm/src/helpers/http.helpers.d.ts.map +1 -0
  26. package/esm/src/helpers/http.helpers.js +2 -0
  27. package/esm/src/helpers/strings.helpers.d.ts +18 -0
  28. package/esm/src/helpers/strings.helpers.d.ts.map +1 -0
  29. package/esm/src/helpers/strings.helpers.js +47 -0
  30. package/esm/src/introspect.d.ts +36 -0
  31. package/esm/src/introspect.d.ts.map +1 -0
  32. package/esm/src/introspect.js +25 -0
  33. package/esm/src/modules/async.d.ts +27 -0
  34. package/esm/src/modules/async.d.ts.map +1 -0
  35. package/esm/src/modules/async.js +38 -0
  36. package/esm/src/modules/http.branded.d.ts +42 -0
  37. package/esm/src/modules/http.branded.d.ts.map +1 -0
  38. package/esm/src/modules/http.branded.js +24 -0
  39. package/esm/src/modules/http.d.ts +46 -0
  40. package/esm/src/modules/http.d.ts.map +1 -0
  41. package/esm/src/modules/http.js +59 -0
  42. package/esm/src/modules/strings.branded.d.ts +185 -0
  43. package/esm/src/modules/strings.branded.d.ts.map +1 -0
  44. package/esm/src/modules/strings.branded.js +123 -0
  45. package/esm/src/modules/strings.d.ts +109 -0
  46. package/esm/src/modules/strings.d.ts.map +1 -0
  47. package/esm/src/modules/strings.js +147 -0
  48. package/esm/src/types.d.ts +318 -0
  49. package/esm/src/types.d.ts.map +1 -0
  50. package/esm/src/types.js +1 -0
  51. package/esm/src/utilities.d.ts +95 -0
  52. package/esm/src/utilities.d.ts.map +1 -0
  53. package/esm/src/utilities.js +196 -0
  54. package/package.json +40 -0
  55. package/script/mod.d.ts +55 -0
  56. package/script/mod.d.ts.map +1 -0
  57. package/script/mod.js +83 -0
  58. package/script/package.json +3 -0
  59. package/script/specs/standard-schema-spec.v1.d.ts +56 -0
  60. package/script/specs/standard-schema-spec.v1.d.ts.map +1 -0
  61. package/script/specs/standard-schema-spec.v1.js +2 -0
  62. package/script/src/batch.d.ts +23 -0
  63. package/script/src/batch.d.ts.map +1 -0
  64. package/script/src/batch.js +26 -0
  65. package/script/src/brand.d.ts +62 -0
  66. package/script/src/brand.d.ts.map +1 -0
  67. package/script/src/brand.js +12 -0
  68. package/script/src/context.d.ts +19 -0
  69. package/script/src/context.d.ts.map +1 -0
  70. package/script/src/context.js +45 -0
  71. package/script/src/extend.d.ts +23 -0
  72. package/script/src/extend.d.ts.map +1 -0
  73. package/script/src/extend.js +12 -0
  74. package/script/src/guard.d.ts +288 -0
  75. package/script/src/guard.d.ts.map +1 -0
  76. package/script/src/guard.js +640 -0
  77. package/script/src/helpers/http.helpers.d.ts +3 -0
  78. package/script/src/helpers/http.helpers.d.ts.map +1 -0
  79. package/script/src/helpers/http.helpers.js +5 -0
  80. package/script/src/helpers/strings.helpers.d.ts +18 -0
  81. package/script/src/helpers/strings.helpers.d.ts.map +1 -0
  82. package/script/src/helpers/strings.helpers.js +52 -0
  83. package/script/src/introspect.d.ts +36 -0
  84. package/script/src/introspect.d.ts.map +1 -0
  85. package/script/src/introspect.js +31 -0
  86. package/script/src/modules/async.d.ts +27 -0
  87. package/script/src/modules/async.d.ts.map +1 -0
  88. package/script/src/modules/async.js +41 -0
  89. package/script/src/modules/http.branded.d.ts +42 -0
  90. package/script/src/modules/http.branded.d.ts.map +1 -0
  91. package/script/src/modules/http.branded.js +30 -0
  92. package/script/src/modules/http.d.ts +46 -0
  93. package/script/src/modules/http.d.ts.map +1 -0
  94. package/script/src/modules/http.js +62 -0
  95. package/script/src/modules/strings.branded.d.ts +185 -0
  96. package/script/src/modules/strings.branded.d.ts.map +1 -0
  97. package/script/src/modules/strings.branded.js +126 -0
  98. package/script/src/modules/strings.d.ts +109 -0
  99. package/script/src/modules/strings.d.ts.map +1 -0
  100. package/script/src/modules/strings.js +150 -0
  101. package/script/src/types.d.ts +318 -0
  102. package/script/src/types.d.ts.map +1 -0
  103. package/script/src/types.js +2 -0
  104. package/script/src/utilities.d.ts +95 -0
  105. package/script/src/utilities.d.ts.map +1 -0
  106. package/script/src/utilities.js +207 -0
package/README.md ADDED
@@ -0,0 +1,901 @@
1
+ # Guardis
2
+
3
+ Guardis is an unopinionated validation and type guard system that prioritizes using TypeScript types
4
+ to define your validation logic. Let your _**types**_ dictate validation rather than having your
5
+ validation library dictate your types.
6
+
7
+ Use the included type guards to perform validation or quickly generate your own anywhere in your
8
+ code, using your existing type definitions.
9
+
10
+ ```ts
11
+ import { createTypeGuard, isNumber, isString } from "jsr:@spudlabs/guardis";
12
+
13
+ // Use built-in guards
14
+ if (isString(userInput)) {
15
+ console.log(userInput.toUpperCase()); // TypeScript knows this is a string
16
+ }
17
+
18
+ // Create guards from shapes — types are inferred automatically
19
+ const isUser = createTypeGuard({ name: isString, age: isNumber });
20
+
21
+ // Or from parser functions for full control
22
+ const isPositive = createTypeGuard<number>((val) =>
23
+ typeof val === "number" && val > 0 ? val : null
24
+ );
25
+ ```
26
+
27
+ ## Features
28
+
29
+ - **Type-First**: Define TypeScript types first, validation follows
30
+ - **Zero Dependencies**: No runtime dependencies
31
+ - **Multiple Modes**: Basic, strict (throws), assert, optional, notEmpty, and validate variants
32
+ - **StandardSchemaV1 Compatible**: Built-in `validate` method returns structured results with detailed error messages
33
+ - **Helper Functions**: Built-in utilities for object, array and tuple validation
34
+ - **Extensible**: Create custom guards, extend existing guards, and extend the core library
35
+ - **Modular**: Import only what you need
36
+
37
+ ## Installation
38
+
39
+ ```bash
40
+ # Deno
41
+ deno install jsr:@spudlabs/guardis
42
+
43
+ # Node.js/npm
44
+ npx jsr add @spudlabs/guardis
45
+ ```
46
+
47
+
48
+ ## Table of Contents
49
+
50
+ - [Features](#features)
51
+ - [Installation](#installation)
52
+ - [Quick Start](#quick-start)
53
+ - [Type Guard Modes](#type-guard-modes)
54
+ - [Basic Mode](#basic-mode)
55
+ - [Strict Mode (Throws Errors)](#strict-mode-throws-errors)
56
+ - [Assert Mode (TypeScript Assertions)](#assert-mode-typescript-assertions)
57
+ - [Optional Mode](#optional-mode)
58
+ - [NotEmpty Mode](#notempty-mode)
59
+ - [Validate Mode (StandardSchemaV1)](#validate-mode-standardschemav1)
60
+ - [Creating Custom Type Guards](#creating-custom-type-guards)
61
+ - [Extending Type Guards](#extending-type-guards)
62
+ - [Specialized Modules](#specialized-modules)
63
+ - [Batch Creation](#batch-creation)
64
+ - [Extending the Is Object](#extending-the-is-object)
65
+ - [Real-World Examples](#real-world-examples)
66
+ - [API Response Validation](#api-response-validation)
67
+ - [Form Validation](#form-validation)
68
+ - [TypeScript Integration](#typescript-integration)
69
+
70
+ ## Quick Start
71
+
72
+ ### Built-in Type Guards
73
+
74
+ Guardis provides type guards for all common JavaScript types:
75
+
76
+ ```ts
77
+ import { Is } from "jsr:@spudlabs/guardis";
78
+
79
+ // Primitives
80
+ Is.String("hello"); // true
81
+ Is.Number(42); // true
82
+ Is.Boolean(true); // true
83
+ Is.Null(null); // true
84
+ Is.Undefined(undefined); // true
85
+
86
+ // Collections
87
+ Is.Array([1, 2, 3]); // true
88
+ Is.Object({ key: "value" }); // true
89
+
90
+ // Special types
91
+ Is.Date(new Date()); // true
92
+ Is.Function(() => {}); // true
93
+ Is.Iterable([1, 2, 3]); // true (arrays, sets, maps, etc.)
94
+ Is.Tuple([1, 2], 2); // true (array with exact length)
95
+
96
+ // JSON-safe types
97
+ Is.JsonValue({ a: 1, b: "text" }); // true
98
+ Is.JsonObject({ key: "value" }); // true
99
+ Is.JsonArray([1, "two", true]); // true
100
+ ```
101
+
102
+ ### Individual Imports
103
+
104
+ Import specific guards to keep bundles small:
105
+
106
+ ```ts
107
+ import { isArray, isNumber, isString } from "jsr:@spudlabs/guardis";
108
+
109
+ if (isString(userInput)) {
110
+ console.log(userInput.trim());
111
+ }
112
+
113
+ if (isNumber(userInput)) {
114
+ return userInput * 10;
115
+ }
116
+
117
+ if (isArray(userValues)) {
118
+ return userValues.at(-1);
119
+ }
120
+ ```
121
+
122
+ ## Type Guard Modes
123
+
124
+ Every type guard comes with multiple modes for different use cases:
125
+
126
+ ### Basic Mode
127
+
128
+ ```ts
129
+ if (Is.String(value)) {
130
+ // TypeScript knows value is a string here
131
+ console.log(value.toUpperCase());
132
+ }
133
+ ```
134
+
135
+ ### Optional Mode
136
+
137
+ ```ts
138
+ // Allows undefined values
139
+ Is.String.optional(value); // true for strings OR undefined
140
+ Is.Number.optional(undefined); // true
141
+ Is.Number.optional(42); // true
142
+ Is.Number.optional("hello"); // false
143
+ ```
144
+
145
+ ### NotEmpty Mode
146
+
147
+ ```ts
148
+ // Rejects "empty" values (null, undefined, "", [], {})
149
+ Is.String.notEmpty("hello"); // true
150
+ Is.String.notEmpty(""); // false
151
+ Is.Array.notEmpty([1, 2, 3]); // true
152
+ Is.Array.notEmpty([]); // false
153
+ ```
154
+
155
+ ### Strict Mode (Throws Errors)
156
+
157
+ Throws a `TypeError` if the predicate fails. Error messages include the expected type name, the received value, and path information for nested validations.
158
+
159
+ ```ts
160
+ // Throws TypeError if validation fails
161
+ Is.String.strict(value);
162
+
163
+ // Error messages include type name and received value
164
+ Is.String.strict(123);
165
+ // TypeError: Expected string. Received: 123
166
+
167
+ Is.Number.strict("hello");
168
+ // TypeError: Expected number. Received: "hello"
169
+
170
+ // With custom error message (overrides default)
171
+ Is.Number.strict(value, "Expected a number for calculation");
172
+ ```
173
+
174
+ **Path tracking for nested validations:**
175
+
176
+ When validating nested objects, strict mode includes the path to the failing property:
177
+
178
+ ```ts
179
+ const isAddress = createTypeGuard("Address", (v, { has }) =>
180
+ Is.Object(v) && has(v, "city", Is.String) && has(v, "zip", Is.Number) ? v : null
181
+ );
182
+
183
+ const isPerson = createTypeGuard("Person", (v, { has }) =>
184
+ Is.Object(v) && has(v, "name", Is.String) && has(v, "address", isAddress) ? v : null
185
+ );
186
+
187
+ isPerson.strict({ name: "Alice", address: { city: 456, zip: 12345 } });
188
+ // TypeError: Expected string. Received: 456 at path: address.city
189
+
190
+ // Array indices are also included in paths
191
+ const isStringArray = Is.Array.of(Is.String);
192
+ isStringArray.strict(["a", "b", 123, "d"]);
193
+ // TypeError: Expected string. Received: 123 at path: 2
194
+ ```
195
+
196
+ **Fail-fast behavior:**
197
+
198
+ Strict mode throws on the first validation failure, unlike `validate()` which collects all errors:
199
+
200
+ ```ts
201
+ isPerson.strict({ name: 123, address: { city: 456 } });
202
+ // TypeError: Expected string. Received: 123 at path: name
203
+ // (Only first error - stops immediately)
204
+
205
+ isPerson.validate({ name: 123, address: { city: 456 } });
206
+ // { issues: [
207
+ // { message: "Expected string. Received: 123", path: ["name"] },
208
+ // { message: "Expected string. Received: 456", path: ["address", "city"] }
209
+ // ]}
210
+ // (Collects all errors)
211
+ ```
212
+
213
+ ### Assert Mode (TypeScript Assertions)
214
+
215
+ It's a requirement of the TypeScript language that all assertion functions have an explicit type annotation. For that reason, in order for TypeScript to recognize that any use of the variable after `assertIsString` can safely consider the `value` to be a string, you have to explicitly set the type to itself.
216
+
217
+ See https://github.com/microsoft/TypeScript/issues/47945 for more information.
218
+
219
+ ```ts
220
+ // For TypeScript assertion functions
221
+ const assertIsString: typeof Is.String.assert = Is.String.assert;
222
+
223
+ assertIsString(value);
224
+
225
+ console.log(value.toUpperCase()); // TypeScript now knows value is a string
226
+ ```
227
+
228
+ ### Validate Mode (StandardSchemaV1)
229
+
230
+ The `validate` method provides [StandardSchemaV1](https://github.com/standard-schema/standard-schema) compatibility, returning structured results with detailed error messages instead of throwing or returning booleans.
231
+
232
+ ```ts
233
+ // Valid inputs return { value: T }
234
+ Is.String.validate("hello");
235
+ // { value: "hello" }
236
+
237
+ Is.Number.validate(42);
238
+ // { value: 42 }
239
+
240
+ // Invalid inputs return { issues: [{ message: string }] }
241
+ Is.String.validate(123);
242
+ // { issues: [{ message: 'Expected string. Received: 123' }] }
243
+
244
+ Is.Number.validate("not a number");
245
+ // { issues: [{ message: 'Expected number. Received: "not a number"' }] }
246
+
247
+ Is.Boolean.validate(null);
248
+ // { issues: [{ message: 'Expected boolean. Received: null' }] }
249
+ ```
250
+
251
+ Error messages include the expected type name and a JSON representation of the received value:
252
+
253
+ ```ts
254
+ // Complex types show detailed error messages
255
+ Is.Array.validate({ key: "value" });
256
+ // { issues: [{ message: 'Expected array. Received: {"key":"value"}' }] }
257
+
258
+ // Union types show combined type names
259
+ Is.Nil.validate("string");
260
+ // { issues: [{ message: 'Expected null | undefined. Received: "string"' }] }
261
+
262
+ // notEmpty variants include the constraint in the error
263
+ Is.String.notEmpty.validate("");
264
+ // { issues: [{ message: 'Expected non-empty string. Received: ""' }] }
265
+
266
+ Is.Array.notEmpty.validate([]);
267
+ // { issues: [{ message: 'Expected non-empty array. Received: []' }] }
268
+ ```
269
+
270
+ **Path tracking for nested validations:**
271
+
272
+ When validating nested objects or arrays, each issue includes a `path` array showing where the error occurred:
273
+
274
+ ```ts
275
+ const isAddress = createTypeGuard("Address", (v, { has }) =>
276
+ Is.Object(v) && has(v, "city", Is.String) && has(v, "zip", Is.Number) ? v : null
277
+ );
278
+
279
+ const isPerson = createTypeGuard("Person", (v, { has }) =>
280
+ Is.Object(v) && has(v, "name", Is.String) && has(v, "address", isAddress) ? v : null
281
+ );
282
+
283
+ // Nested validation errors include path information
284
+ isPerson.validate({ name: "Alice", address: { city: 123, zip: "invalid" } });
285
+ // {
286
+ // issues: [
287
+ // { message: "Expected string. Received: 123", path: ["address", "city"] },
288
+ // { message: "Expected number. Received: \"invalid\"", path: ["address", "zip"] }
289
+ // ]
290
+ // }
291
+
292
+ // Array validation includes indices in the path
293
+ const isStringArray = Is.Array.of(Is.String);
294
+ isStringArray.validate(["a", 123, "c"]);
295
+ // { issues: [{ message: "Expected string. Received: 123", path: [1] }] }
296
+
297
+ // Deeply nested paths
298
+ const isPeople = Is.Array.of(isPerson);
299
+ isPeople.validate([{ name: "Alice", address: { city: 456, zip: 12345 } }]);
300
+ // { issues: [{ message: "Expected string. Received: 456", path: [0, "address", "city"] }] }
301
+ ```
302
+
303
+ **Collecting errors:**
304
+
305
+ Unlike strict mode which fails fast, `validate()` can collect multiple validation errors. Object validators using `has()` will collect all property errors, while array validators (`Is.Array.of()`) stop at the first invalid element for performance:
306
+
307
+ ```ts
308
+ isPerson.validate({ name: 123, address: { city: 456, zip: "bad" } });
309
+ // {
310
+ // issues: [
311
+ // { message: "Expected string. Received: 123", path: ["name"] },
312
+ // { message: "Expected string. Received: 456", path: ["address", "city"] },
313
+ // { message: "Expected number. Received: \"bad\"", path: ["address", "zip"] }
314
+ // ]
315
+ // }
316
+ ```
317
+
318
+ Works with typed arrays and custom type guards:
319
+
320
+ ```ts
321
+ // Typed arrays
322
+ const isStringArray = Is.Array.of(Is.String);
323
+
324
+ isStringArray.validate(["a", "b", "c"]);
325
+ // { value: ["a", "b", "c"] }
326
+
327
+ isStringArray.validate([1, 2, 3]);
328
+ // { issues: [{ message: 'Expected string. Received: 1', path: [0] }] }
329
+
330
+ // Custom type guards
331
+ const isPositive = Is.Number.extend("positive number", (val) =>
332
+ val > 0 ? val : null
333
+ );
334
+
335
+ isPositive.validate(42);
336
+ // { value: 42 }
337
+
338
+ isPositive.validate(-5);
339
+ // { issues: [{ message: 'Expected positive number. Received: -5' }] }
340
+ ```
341
+
342
+ ## Creating Custom Type Guards
343
+
344
+ Use `createTypeGuard` to build validators for your own types. You can pass either a **shape object** for declarative validation or a **parser function** for full control.
345
+
346
+ ### Shape-Based Validation
347
+
348
+ The simplest way to create a type guard for an object. Pass an object mapping property names to guards — the TypeScript type is inferred automatically:
349
+
350
+ ```ts
351
+ import { createTypeGuard, isArray, isNumber, isString } from "jsr:@spudlabs/guardis";
352
+
353
+ // Basic shape
354
+ const isUser = createTypeGuard({ name: isString, age: isNumber });
355
+
356
+ isUser({ name: "Alice", age: 30 }); // true
357
+ isUser({ name: "Alice", age: "thirty" }); // false
358
+
359
+ // Type is inferred as { name: string; age: number }
360
+ type User = typeof isUser._TYPE;
361
+ ```
362
+
363
+ Shapes support all guard modes as field values:
364
+
365
+ ```ts
366
+ const isForm = createTypeGuard({
367
+ name: isString.notEmpty, // rejects empty strings
368
+ email: isString,
369
+ nickname: isString.optional, // accepts undefined
370
+ role: isString.or(isNumber), // union type
371
+ tags: isArray.of(isString), // typed array
372
+ });
373
+ ```
374
+
375
+ Shapes can be nested:
376
+
377
+ ```ts
378
+ const isAddress = createTypeGuard({
379
+ street: isString,
380
+ city: isString,
381
+ zip: isNumber,
382
+ });
383
+
384
+ const isPerson = createTypeGuard({
385
+ name: isString,
386
+ address: isAddress, // nested TypeGuard
387
+ });
388
+
389
+ // Or inline:
390
+ const isPerson2 = createTypeGuard({
391
+ name: isString,
392
+ address: { street: isString, city: isString, zip: isNumber },
393
+ });
394
+ ```
395
+
396
+ Named shapes provide better error messages in strict mode:
397
+
398
+ ```ts
399
+ const isUser = createTypeGuard("User", { name: isString, age: isNumber });
400
+ ```
401
+
402
+ All modes work on shape-created guards — strict, assert, optional, notEmpty, validate, or, and extend:
403
+
404
+ ```ts
405
+ const isUser = createTypeGuard({ name: isString, age: isNumber });
406
+
407
+ isUser.strict({ name: 123, age: 30 });
408
+ // TypeError: Expected string. Received: 123 at path: name
409
+
410
+ isUser.validate({ name: 123, age: "old" });
411
+ // { issues: [
412
+ // { message: "Expected string. Received: 123", path: ["name"] },
413
+ // { message: "Expected number. Received: \"old\"", path: ["age"] }
414
+ // ]}
415
+
416
+ const isAdult = isUser.extend((val) => val.age >= 18 ? val : null);
417
+ const isUserOrString = isUser.or(isString);
418
+ ```
419
+
420
+ ### Parser-Based Validation
421
+
422
+ For validation logic that goes beyond property checks, use a parser function:
423
+
424
+ ### Simple Types
425
+
426
+ ```ts
427
+ import { createTypeGuard } from "jsr:@spudlabs/guardis";
428
+
429
+ type Status = "pending" | "complete" | "failed";
430
+
431
+ const isStatus = createTypeGuard((val, { includes }) => {
432
+ const validStatuses: Status[] = ["pending", "complete", "failed"];
433
+ return isString(val) && includes(validStatuses, val) ? val : null;
434
+ });
435
+
436
+ // All modes available automatically
437
+ isStatus("pending"); // true
438
+ isStatus.strict("invalid"); // throws TypeError
439
+ isStatus.optional(undefined); // true
440
+ ```
441
+
442
+ ### Complex Objects
443
+
444
+ Use helper functions for object validation:
445
+
446
+ ```ts
447
+ type User = {
448
+ id: number;
449
+ name: string;
450
+ email?: string; // optional property
451
+ };
452
+
453
+ const isUser = createTypeGuard<User>((val, { has, hasOptional }) => {
454
+ if (!Is.Object(val)) return null;
455
+
456
+ if (
457
+ has(val, "id", Is.Number) &&
458
+ has(val, "name", Is.String) &&
459
+ hasOptional(val, "email", Is.String)
460
+ ) {
461
+ return val;
462
+ }
463
+
464
+ return null;
465
+ });
466
+ ```
467
+
468
+ ### Available Helpers
469
+
470
+ ```ts
471
+ const isExample = createTypeGuard((val, helpers) => {
472
+ const { has, hasOptional, hasNot, tupleHas, includes, keyOf, fail } = helpers;
473
+
474
+ // Check required object property
475
+ has(obj, "key", Is.String);
476
+
477
+ // Check required property with custom error message
478
+ has(obj, "email", Is.String, "Email is required");
479
+
480
+ // Check optional object property
481
+ hasOptional(obj, "optional", Is.Number); // { optional?: number | undefined }
482
+
483
+ // Check that a property does NOT exist
484
+ hasNot(obj, "deleted"); // ensures "deleted" property is absent
485
+
486
+ // Check tuple element at specific index
487
+ tupleHas(tuple, 0, Is.String); // [string, ...unknown[]]
488
+
489
+ // Check if value is in array (for union types)
490
+ const colors = ["red", "blue", "green"] as const;
491
+ includes(colors, val); // "red" | "blue" | "green"
492
+
493
+ // Check if a key exists in an object
494
+ keyOf(key, someObject); // key is keyof typeof someObject
495
+
496
+ // Return custom validation error (works with validate and strict modes)
497
+ if (val.age < 0) return fail("Age must be non-negative");
498
+
499
+ return val;
500
+ });
501
+ ```
502
+
503
+ **Custom error messages:**
504
+
505
+ The `has`, `hasOptional`, `keyOf`, and `fail` helpers support custom error messages that appear in validation results and strict mode errors:
506
+
507
+ ```ts
508
+ const isPerson = createTypeGuard("Person", (v, { has, fail }) => {
509
+ if (!Is.Object(v)) return fail("Value must be an object");
510
+ if (!has(v, "name", Is.String, "Name is required and must be a string")) return null;
511
+ if (!has(v, "age", Is.Number, "Age must be a number")) return null;
512
+
513
+ const person = v as { name: string; age: number };
514
+ if (person.age < 0) return fail("Age must be non-negative");
515
+ if (person.age > 150) return fail("Age must be realistic");
516
+
517
+ return v;
518
+ });
519
+
520
+ // Custom messages appear in validate results
521
+ isPerson.validate({ name: 123 });
522
+ // { issues: [{ message: "Name is required and must be a string", path: ["name"] }] }
523
+
524
+ // And in strict mode errors
525
+ isPerson.strict({ age: -5, name: "Alice" });
526
+ // TypeError: Age must be non-negative
527
+ ```
528
+
529
+ ## Extending Type Guards
530
+
531
+ The `extend` method allows you to build upon existing type guards by adding additional validation rules. This is particularly useful when you need to add constraints or refinements to a base type.
532
+
533
+ ### Basic Extension
534
+
535
+ ```ts
536
+ // Extend isString to validate email format
537
+ const isEmail = isString.extend((val) => {
538
+ return val.includes("@") && val.includes(".") ? val : null;
539
+ });
540
+
541
+ isEmail("user@example.com"); // true
542
+ isEmail("invalid-email"); // false
543
+ isEmail(123); // false (fails base validation)
544
+ ```
545
+
546
+ ### Number Range Validation
547
+
548
+ ```ts
549
+ // Extend isNumber to create a percentage validator (0-100)
550
+ const isPercentage = isNumber.extend((val) => {
551
+ return val >= 0 && val <= 100 ? val : null;
552
+ });
553
+
554
+ isPercentage(50); // true
555
+ isPercentage(150); // false
556
+ isPercentage("50"); // false
557
+ ```
558
+
559
+ ### String Pattern Validation
560
+
561
+ ```ts
562
+ // Extend isString to validate phone numbers
563
+ const isPhoneNumber = isString.extend((val) => {
564
+ return /^\d{3}-\d{3}-\d{4}$/.test(val) ? val : null;
565
+ });
566
+
567
+ isPhoneNumber("555-123-4567"); // true
568
+ isPhoneNumber("invalid"); // false
569
+ ```
570
+
571
+ ### Object Property Refinement
572
+
573
+ ```ts
574
+ type User = { name: string; age: number };
575
+
576
+ const isUser = createTypeGuard<User>((val, { has }) => {
577
+ if (Is.Object(val) && has(val, "name", Is.String) && has(val, "age", Is.Number)) {
578
+ return val;
579
+ }
580
+ return null;
581
+ });
582
+
583
+ // Extend to only accept adults
584
+ const isAdult = isUser.extend((val) => {
585
+ return val.age >= 18 ? val : null;
586
+ });
587
+
588
+ isAdult({ name: "Alice", age: 25 }); // true
589
+ isAdult({ name: "Bob", age: 16 }); // false
590
+ ```
591
+
592
+ ### Chained Extensions
593
+
594
+ Extensions can be chained to create increasingly specific validators:
595
+
596
+ ```ts
597
+ const isPositiveNumber = isNumber.extend((val) =>
598
+ val > 0 ? val : null
599
+ );
600
+
601
+ const isPositiveInteger = isPositiveNumber.extend((val) =>
602
+ Number.isInteger(val) ? val : null
603
+ );
604
+
605
+ const isEvenPositiveInteger = isPositiveInteger.extend((val) =>
606
+ val % 2 === 0 ? val : null
607
+ );
608
+
609
+ isEvenPositiveInteger(10); // true
610
+ isEvenPositiveInteger(9); // false (not even)
611
+ isEvenPositiveInteger(3.5); // false (not integer)
612
+ isEvenPositiveInteger(-2); // false (not positive)
613
+ ```
614
+
615
+ ### Extensions with Helper Functions
616
+
617
+ Extended validators have access to the same helper functions as the base validators:
618
+
619
+ ```ts
620
+ // Create a status type with specific allowed values
621
+ const validStatuses = ["active", "inactive", "pending"] as const;
622
+
623
+ const isStatus = isString.extend((val, { includes }) => {
624
+ if (includes(validStatuses, val)) {
625
+ return val;
626
+ }
627
+
628
+ return null;
629
+ });
630
+
631
+ isStatus("active"); // true
632
+ isStatus("completed"); // false
633
+ ```
634
+
635
+ ### All Modes Work with Extensions
636
+
637
+ Extended type guards support all the same modes as base type guards:
638
+
639
+ ```ts
640
+ const isPositiveNumber = isNumber.extend((val) =>
641
+ val > 0 ? val : null
642
+ );
643
+
644
+ // Basic mode
645
+ isPositiveNumber(10); // true
646
+
647
+ // Strict mode (throws on failure)
648
+ isPositiveNumber.strict(10); // passes
649
+ isPositiveNumber.strict(-5); // throws TypeError
650
+
651
+ // Assert mode
652
+ const assertIsPositive: typeof isPositiveNumber.assert = isPositiveNumber.assert;
653
+ assertIsPositive(10); // passes
654
+ // assertIsPositive(-5); // throws
655
+
656
+ // Optional mode
657
+ isPositiveNumber.optional(10); // true
658
+ isPositiveNumber.optional(undefined); // true
659
+ isPositiveNumber.optional(-5); // false
660
+ ```
661
+
662
+ ## Specialized Modules
663
+
664
+ Guardis includes specialized modules for domain-specific types:
665
+
666
+ ### Async Module
667
+
668
+ ```ts
669
+ import { isAsyncFunction, isPromise, isPromiseLike, isThenable } from "jsr:@spudlabs/guardis/async";
670
+
671
+ // Async types
672
+ isAsyncFunction(async () => {}); // true
673
+ isPromise(Promise.resolve(42)); // true
674
+ isPromiseLike({ then: () => {} }); // true (checks for .then method)
675
+ isThenable({ then: () => {} }); // true (alias for isPromiseLike)
676
+
677
+ // All modes available
678
+ isPromise.strict(value); // throws if not Promise
679
+ isAsyncFunction.optional(value); // allows undefined
680
+ ```
681
+
682
+ ### HTTP Module
683
+
684
+ ```ts
685
+ import { isNativeURL, isRequest, isResponse } from "jsr:@spudlabs/guardis/http";
686
+
687
+ // Web API types
688
+ isNativeURL(new URL("https://example.com")); // true
689
+ isRequest(new Request("https://api.com")); // true
690
+ isResponse(new Response("data")); // true
691
+
692
+ // All modes available
693
+ isRequest.strict(value); // throws if not Request
694
+ isResponse.optional(value); // allows undefined
695
+ ```
696
+
697
+ ### String Module
698
+
699
+ ```ts
700
+ import { isEmail, isInternationalPhone, isUSPhone, isPhoneNumber, isUUIDv4, isCommaDelimited } from "jsr:@spudlabs/guardis/strings";
701
+
702
+ // Email validation
703
+ isEmail("user@example.com"); // true
704
+ isEmail("invalid-email"); // false
705
+
706
+ // Phone number validation
707
+ isInternationalPhone("+1 234 567 8901"); // true (international format)
708
+ isUSPhone("555-123-4567"); // true (US format)
709
+ isPhoneNumber("+44 20 7946 0958"); // true (accepts both formats)
710
+
711
+ // UUID v4 validation
712
+ isUUIDv4("550e8400-e29b-41d4-a716-446655440000"); // true
713
+ isUUIDv4("invalid-uuid"); // false
714
+
715
+ // Comma-delimited string validation (for CSV-like data)
716
+ isCommaDelimited("value1,value2,value3"); // true
717
+ isCommaDelimited('"quoted,value",unquoted'); // true (supports quoted values)
718
+
719
+ // All modes available
720
+ isEmail.strict(value); // throws if not a valid email
721
+ isPhoneNumber.optional(value); // allows undefined
722
+ ```
723
+
724
+ ## Batch Creation
725
+
726
+ Generate multiple type guards at once. Accepts parser functions, named parsers, or shapes:
727
+
728
+ ```ts
729
+ import { batch, isNumber, isString } from "jsr:@spudlabs/guardis";
730
+
731
+ // Parser functions
732
+ const { isRed, isBlue, isGreen } = batch({
733
+ Red: (val) => val === "red" ? val : null,
734
+ Blue: (val) => val === "blue" ? val : null,
735
+ Green: (val) => val === "green" ? val : null,
736
+ });
737
+
738
+ // Shapes
739
+ const { isPerson, isAddress } = batch({
740
+ Person: { name: isString, age: isNumber },
741
+ Address: { street: isString, city: isString, zip: isNumber },
742
+ });
743
+
744
+ // Mix parsers and shapes
745
+ const { isColor, isUser } = batch({
746
+ Color: (val) => typeof val === "string" && ["red", "green", "blue"].includes(val) ? val : null,
747
+ User: { name: isString, email: isString },
748
+ });
749
+
750
+ // All guards get full mode support
751
+ isRed.strict("blue"); // throws
752
+ isPerson.validate({ name: 123 }); // { issues: [...] }
753
+ ```
754
+
755
+ ## Extending the Is Object
756
+
757
+ Add your own type guards to the `Is` namespace. Accepts parser functions, named parsers, or shapes:
758
+
759
+ ```ts
760
+ import { extend, Is as BaseIs, isNumber, isString } from "jsr:@spudlabs/guardis";
761
+
762
+ // Create new Is object with custom guards
763
+ const Is = extend(BaseIs, {
764
+ Email: (val) => typeof val === "string" && val.includes("@") ? val : null,
765
+ PositiveNumber: (val) => typeof val === "number" && val > 0 ? val : null,
766
+ User: { name: isString, age: isNumber }, // shapes work too
767
+ });
768
+
769
+ // Use built-in and custom guards together
770
+ Is.String("hello"); // built-in
771
+ Is.Email("user@domain.com"); // custom
772
+ Is.User({ name: "Alice", age: 30 }); // shape-based
773
+
774
+ // All modes work with custom guards
775
+ Is.Email.strict(invalidEmail); // throws
776
+ Is.User.validate({ name: 123 }); // { issues: [...] }
777
+ ```
778
+
779
+ ## Real-World Examples
780
+
781
+ ### API Response Validation
782
+
783
+ ```ts
784
+ type ApiResponse<T> = {
785
+ success: boolean;
786
+ data?: T;
787
+ error?: string;
788
+ };
789
+
790
+ const isApiResponse = <T>(dataGuard: (val: unknown) => val is T) =>
791
+ createTypeGuard<ApiResponse<T>>((val, { has, hasOptional }) => {
792
+ if (!Is.Object(val)) return null;
793
+
794
+ if (
795
+ has(val, "success", Is.Boolean) &&
796
+ hasOptional(val, "data", dataGuard) &&
797
+ hasOptional(val, "error", Is.String)
798
+ ) {
799
+ return val;
800
+ }
801
+
802
+ return null;
803
+ });
804
+
805
+ // Usage
806
+ const isUserResponse = isApiResponse(isUser);
807
+ if (isUserResponse(response)) {
808
+ console.log(response.data?.name); // TypeScript knows the shape
809
+ }
810
+ ```
811
+
812
+ ### Form Validation
813
+
814
+ ```ts
815
+ type ContactForm = {
816
+ name: string;
817
+ email: string;
818
+ age: number;
819
+ newsletter: boolean;
820
+ };
821
+
822
+ const isContactForm = createTypeGuard<ContactForm>((val, { has }) => {
823
+ if (!Is.Object(val)) return null;
824
+
825
+ if (
826
+ has(val, "name", Is.String) &&
827
+ has(val, "email", (v) => Is.String(v) && v.includes("@") ? v : null) &&
828
+ has(val, "age", (v) => Is.Number(v) && v >= 0 ? v : null) &&
829
+ has(val, "newsletter", Is.Boolean)
830
+ ) {
831
+ return val;
832
+ }
833
+
834
+ return null;
835
+ });
836
+
837
+ // Use in form handler
838
+ function handleSubmit(formData: unknown) {
839
+ try {
840
+ isContactForm.strict(formData, "Invalid form data");
841
+ // formData is now typed as ContactForm
842
+ saveContact(formData);
843
+ } catch (error) {
844
+ showError(error.message);
845
+ }
846
+ }
847
+ ```
848
+
849
+ ## TypeScript Integration
850
+
851
+ Guardis is designed to work seamlessly with TypeScript:
852
+
853
+ - **Type Narrowing**: Guards narrow types in `if` statements
854
+ - **Assertion Functions**: `.assert()` methods work as TypeScript assertions
855
+ - **Generic Support**: Create guards for generic types
856
+ - **Strict Typing**: All guards are fully typed with proper inference
857
+ - **Type Inference**: Extract types from guards using the `_TYPE` property
858
+
859
+ ```ts
860
+ function processData(input: unknown) {
861
+ if (Is.Array(input)) {
862
+ // TypeScript knows input is unknown[]
863
+ input.forEach((item) => {/* ... */});
864
+ }
865
+
866
+ // Assertion style
867
+ const assertIsString: typeof Is.String.assert = Is.String.assert;
868
+ assertIsString(input);
869
+ // TypeScript knows input is string after this line
870
+ }
871
+ ```
872
+
873
+ ### Type Inference with `_TYPE`
874
+
875
+ Every type guard includes a `_TYPE` property that allows you to extract the guarded type for use in other parts of your code:
876
+
877
+ ```ts
878
+ // Extract the type from a built-in guard
879
+ type StringType = typeof Is.String._TYPE; // string
880
+ type NumberType = typeof Is.Number._TYPE; // number
881
+
882
+ // Extract the type from a custom guard
883
+ type User = { id: number; name: string };
884
+ const isUser = createTypeGuard<User>((val, { has }) =>
885
+ has(val, "id", Is.Number) && has(val, "name", Is.String) ? val : null
886
+ );
887
+
888
+ // Use _TYPE to infer the type elsewhere
889
+ type UserType = typeof isUser._TYPE; // { id: number; name: string }
890
+
891
+ // Useful for creating related types
892
+ type UserArray = Array<typeof isUser._TYPE>;
893
+ type UserResponse = {
894
+ success: boolean;
895
+ user: typeof isUser._TYPE;
896
+ };
897
+
898
+ // Works with extended guards too
899
+ const isAdult = isUser.extend((val) => val.age >= 18 ? val : null);
900
+ type AdultType = typeof isAdult._TYPE; // inferred type from extension
901
+ ```