@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.
- package/README.md +901 -0
- package/esm/mod.d.ts +55 -0
- package/esm/mod.d.ts.map +1 -0
- package/esm/mod.js +43 -0
- package/esm/package.json +3 -0
- package/esm/specs/standard-schema-spec.v1.d.ts +56 -0
- package/esm/specs/standard-schema-spec.v1.d.ts.map +1 -0
- package/esm/specs/standard-schema-spec.v1.js +1 -0
- package/esm/src/batch.d.ts +23 -0
- package/esm/src/batch.d.ts.map +1 -0
- package/esm/src/batch.js +23 -0
- package/esm/src/brand.d.ts +62 -0
- package/esm/src/brand.d.ts.map +1 -0
- package/esm/src/brand.js +9 -0
- package/esm/src/context.d.ts +19 -0
- package/esm/src/context.d.ts.map +1 -0
- package/esm/src/context.js +41 -0
- package/esm/src/extend.d.ts +23 -0
- package/esm/src/extend.d.ts.map +1 -0
- package/esm/src/extend.js +9 -0
- package/esm/src/guard.d.ts +288 -0
- package/esm/src/guard.d.ts.map +1 -0
- package/esm/src/guard.js +631 -0
- package/esm/src/helpers/http.helpers.d.ts +3 -0
- package/esm/src/helpers/http.helpers.d.ts.map +1 -0
- package/esm/src/helpers/http.helpers.js +2 -0
- package/esm/src/helpers/strings.helpers.d.ts +18 -0
- package/esm/src/helpers/strings.helpers.d.ts.map +1 -0
- package/esm/src/helpers/strings.helpers.js +47 -0
- package/esm/src/introspect.d.ts +36 -0
- package/esm/src/introspect.d.ts.map +1 -0
- package/esm/src/introspect.js +25 -0
- package/esm/src/modules/async.d.ts +27 -0
- package/esm/src/modules/async.d.ts.map +1 -0
- package/esm/src/modules/async.js +38 -0
- package/esm/src/modules/http.branded.d.ts +42 -0
- package/esm/src/modules/http.branded.d.ts.map +1 -0
- package/esm/src/modules/http.branded.js +24 -0
- package/esm/src/modules/http.d.ts +46 -0
- package/esm/src/modules/http.d.ts.map +1 -0
- package/esm/src/modules/http.js +59 -0
- package/esm/src/modules/strings.branded.d.ts +185 -0
- package/esm/src/modules/strings.branded.d.ts.map +1 -0
- package/esm/src/modules/strings.branded.js +123 -0
- package/esm/src/modules/strings.d.ts +109 -0
- package/esm/src/modules/strings.d.ts.map +1 -0
- package/esm/src/modules/strings.js +147 -0
- package/esm/src/types.d.ts +318 -0
- package/esm/src/types.d.ts.map +1 -0
- package/esm/src/types.js +1 -0
- package/esm/src/utilities.d.ts +95 -0
- package/esm/src/utilities.d.ts.map +1 -0
- package/esm/src/utilities.js +196 -0
- package/package.json +40 -0
- package/script/mod.d.ts +55 -0
- package/script/mod.d.ts.map +1 -0
- package/script/mod.js +83 -0
- package/script/package.json +3 -0
- package/script/specs/standard-schema-spec.v1.d.ts +56 -0
- package/script/specs/standard-schema-spec.v1.d.ts.map +1 -0
- package/script/specs/standard-schema-spec.v1.js +2 -0
- package/script/src/batch.d.ts +23 -0
- package/script/src/batch.d.ts.map +1 -0
- package/script/src/batch.js +26 -0
- package/script/src/brand.d.ts +62 -0
- package/script/src/brand.d.ts.map +1 -0
- package/script/src/brand.js +12 -0
- package/script/src/context.d.ts +19 -0
- package/script/src/context.d.ts.map +1 -0
- package/script/src/context.js +45 -0
- package/script/src/extend.d.ts +23 -0
- package/script/src/extend.d.ts.map +1 -0
- package/script/src/extend.js +12 -0
- package/script/src/guard.d.ts +288 -0
- package/script/src/guard.d.ts.map +1 -0
- package/script/src/guard.js +640 -0
- package/script/src/helpers/http.helpers.d.ts +3 -0
- package/script/src/helpers/http.helpers.d.ts.map +1 -0
- package/script/src/helpers/http.helpers.js +5 -0
- package/script/src/helpers/strings.helpers.d.ts +18 -0
- package/script/src/helpers/strings.helpers.d.ts.map +1 -0
- package/script/src/helpers/strings.helpers.js +52 -0
- package/script/src/introspect.d.ts +36 -0
- package/script/src/introspect.d.ts.map +1 -0
- package/script/src/introspect.js +31 -0
- package/script/src/modules/async.d.ts +27 -0
- package/script/src/modules/async.d.ts.map +1 -0
- package/script/src/modules/async.js +41 -0
- package/script/src/modules/http.branded.d.ts +42 -0
- package/script/src/modules/http.branded.d.ts.map +1 -0
- package/script/src/modules/http.branded.js +30 -0
- package/script/src/modules/http.d.ts +46 -0
- package/script/src/modules/http.d.ts.map +1 -0
- package/script/src/modules/http.js +62 -0
- package/script/src/modules/strings.branded.d.ts +185 -0
- package/script/src/modules/strings.branded.d.ts.map +1 -0
- package/script/src/modules/strings.branded.js +126 -0
- package/script/src/modules/strings.d.ts +109 -0
- package/script/src/modules/strings.d.ts.map +1 -0
- package/script/src/modules/strings.js +150 -0
- package/script/src/types.d.ts +318 -0
- package/script/src/types.d.ts.map +1 -0
- package/script/src/types.js +2 -0
- package/script/src/utilities.d.ts +95 -0
- package/script/src/utilities.d.ts.map +1 -0
- 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
|
+
```
|