@webergency-utils/typechecker 0.1.9 → 0.1.10
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 +256 -276
- package/dist/engine/staticAsserts.js +1 -1
- package/dist/plugin.d.ts +7 -0
- package/dist/plugin.js +30 -0
- package/dist/runtime/casing.d.ts +3 -3
- package/dist/runtime/casing.js +4 -4
- package/package.json +10 -4
- package/LICENSE +0 -21
package/README.md
CHANGED
|
@@ -1,38 +1,64 @@
|
|
|
1
1
|
# @webergency-utils/typechecker
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
[](https://www.npmjs.com/package/@webergency-utils/typechecker) [](#maintenance) [](https://www.npmjs.com/package/@webergency-utils/typechecker) [](https://www.npmjs.com/package/@webergency-utils/typechecker)
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
An ahead-of-time (AOT) type validation engine and TypeScript compiler plugin that compiles TypeScript types directly into optimized runtime validation functions. It intercepts type definitions at build time to enforce value constraints, formatting, and defaults with zero runtime reflection and no third-party schema dependencies.
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
## TL;DR
|
|
8
8
|
|
|
9
|
-
|
|
9
|
+
```typescript
|
|
10
|
+
import { validate, constraint, format } from '@webergency-utils/typechecker';
|
|
10
11
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
- **📈 Coercion & Coalescing**: Extended options for type conversion and single-value array wrapping (highly useful for HTTP Query parameters!).
|
|
12
|
+
interface User {
|
|
13
|
+
id: string & format.UUID;
|
|
14
|
+
name: string & constraint.MinLength<2>;
|
|
15
|
+
age: number & constraint.Minimum<18>;
|
|
16
|
+
}
|
|
17
17
|
|
|
18
|
-
|
|
18
|
+
const input: unknown = {
|
|
19
|
+
id: '550e8400-e29b-41d4-a716-446655440000',
|
|
20
|
+
name: 'Alice',
|
|
21
|
+
age: 25,
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
// Validate input against the User type definition
|
|
25
|
+
const result = validate<User>(input);
|
|
26
|
+
|
|
27
|
+
if (result.success) {
|
|
28
|
+
// TypeScript narrows type to User here
|
|
29
|
+
console.log('User is valid:', result.data);
|
|
30
|
+
} else {
|
|
31
|
+
console.error('Validation failed:', result.errors);
|
|
32
|
+
}
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
## Installation & Setup
|
|
36
|
+
|
|
37
|
+
Since this package is a TypeScript compiler plugin (transformer), you must compile your project using a compiler patcher like `ts-patch` to hook into TS compilation.
|
|
19
38
|
|
|
20
|
-
|
|
39
|
+
### 1. Install Dependencies
|
|
21
40
|
|
|
22
|
-
|
|
41
|
+
Install the core package, along with `ts-patch` as a development dependency:
|
|
23
42
|
|
|
24
43
|
```bash
|
|
25
44
|
npm install @webergency-utils/typechecker
|
|
26
|
-
npm install -
|
|
45
|
+
npm install --save-dev ts-patch
|
|
27
46
|
```
|
|
28
47
|
|
|
29
|
-
|
|
48
|
+
### 2. Inject Compiler Hook
|
|
30
49
|
|
|
31
|
-
|
|
50
|
+
Run the patcher command to set up `ts-patch` inside your local TypeScript installation:
|
|
32
51
|
|
|
33
|
-
|
|
52
|
+
```bash
|
|
53
|
+
npx ts-patch install
|
|
54
|
+
```
|
|
34
55
|
|
|
35
|
-
|
|
56
|
+
> [!NOTE]
|
|
57
|
+
> It is recommended to add `ts-patch install` to your `package.json` `prepare` script so it runs automatically after every dependency installation.
|
|
58
|
+
|
|
59
|
+
### 3. Configure tsconfig.json
|
|
60
|
+
|
|
61
|
+
Register the typechecker transform plugin under the `plugins` array of `compilerOptions` in your `tsconfig.json`:
|
|
36
62
|
|
|
37
63
|
```json
|
|
38
64
|
{
|
|
@@ -49,329 +75,283 @@ Update your `tsconfig.json` to include the transformer in the `compilerOptions.p
|
|
|
49
75
|
|
|
50
76
|
---
|
|
51
77
|
|
|
52
|
-
##
|
|
78
|
+
## Architecture & Internals
|
|
53
79
|
|
|
54
|
-
|
|
80
|
+
The package utilizes a custom TypeScript compiler transformer and language service plugin to deliver highly efficient type-safe runtime validations.
|
|
55
81
|
|
|
56
|
-
|
|
57
|
-
import { is, assert, assertGuard, validate } from '@webergency-utils/typechecker';
|
|
82
|
+
### Build-Time Compilation Flow
|
|
58
83
|
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
84
|
+
```mermaid
|
|
85
|
+
graph TD
|
|
86
|
+
A[TypeScript Source Code] --> B[ts-patch / Compiler Hook]
|
|
87
|
+
B --> C[TypeScript compiler plugin / Transformer]
|
|
88
|
+
C --> D[Extract types via compiler TypeChecker]
|
|
89
|
+
D --> E[Generate optimized JS validator function]
|
|
90
|
+
E --> F[Hoisted Validator Registry & MetadataStore registration]
|
|
91
|
+
F --> G[Replace source call with MetadataStore invocation]
|
|
92
|
+
G --> H[Emit optimized JavaScript files]
|
|
93
|
+
```
|
|
63
94
|
|
|
64
|
-
|
|
95
|
+
1. **Build-Time Transformation**: The compiler transformer intercepts calls to validation helper functions (`validate`, `is`, `assert`, `assertGuard`, `jsonSchema`) containing generic arguments.
|
|
96
|
+
2. **Type Extraction & Analysis**: It parses the target TS type structure, extracting intersection constraints, formats, transforms, and defaults recursively.
|
|
97
|
+
3. **Hoisted Registry**: The transformer generates highly optimized, direct JavaScript validator functions for each resolved type shape, generates a unique hash, and registers them in a global `MetadataStore`.
|
|
98
|
+
4. **Call Replacement**: The transformer replaces the original compile-time call expressions with direct, zero-reflection references to the runtime `MetadataStore`.
|
|
65
99
|
|
|
66
|
-
|
|
67
|
-
if (is<Payload>(data)) {
|
|
68
|
-
console.log(data.id); // Narrowed to 'Payload'
|
|
69
|
-
}
|
|
100
|
+
### Static Constraint Diagnostics
|
|
70
101
|
|
|
71
|
-
|
|
72
|
-
const validData = assert<Payload>(data);
|
|
102
|
+
The package includes an IDE / Language Service plugin that statically checks literal values against type constraints during editing or compilation:
|
|
73
103
|
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
console.log(data.id); // Narrowed in-place
|
|
104
|
+
```typescript
|
|
105
|
+
import { constraint } from '@webergency-utils/typechecker';
|
|
77
106
|
|
|
78
|
-
//
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
console.log(result.data);
|
|
82
|
-
} else {
|
|
83
|
-
console.error(result.errors); // Array of formatted errors
|
|
84
|
-
}
|
|
107
|
+
// This yields a compilation error directly in the IDE:
|
|
108
|
+
// Type '5' is not assignable to type 'number & Minimum<18>'.
|
|
109
|
+
const age: number & constraint.Minimum<18> = 5;
|
|
85
110
|
```
|
|
86
111
|
|
|
87
112
|
---
|
|
88
113
|
|
|
89
|
-
|
|
114
|
+
## Glossary
|
|
90
115
|
|
|
91
|
-
|
|
116
|
+
- [validate](file:///Users/tomaskorenko/Projects/Github/webergency-utils/typechecker/src/index.ts#L22): Validates a value against a type, returning a structured result containing the validation status and a detailed list of errors.
|
|
117
|
+
- [is](file:///Users/tomaskorenko/Projects/Github/webergency-utils/typechecker/src/index.ts#L19): A type guard function that returns `true` if a value is valid, narrowing its type for TypeScript.
|
|
118
|
+
- [assert](file:///Users/tomaskorenko/Projects/Github/webergency-utils/typechecker/src/index.ts#L20): Validates a value and returns it, throwing a validation error on failure.
|
|
119
|
+
- [assertGuard](file:///Users/tomaskorenko/Projects/Github/webergency-utils/typechecker/src/index.ts#L21): A type assertion function that throws if a value does not match the target type, narrowing the type in the outer scope.
|
|
120
|
+
- [jsonSchema](file:///Users/tomaskorenko/Projects/Github/webergency-utils/typechecker/src/index.ts#L23): Generates and returns a raw JSON Schema draft-07 representation matching a TypeScript type at compile time.
|
|
121
|
+
- [WithModifiers](file:///Users/tomaskorenko/Projects/Github/webergency-utils/typechecker/src/runtime/tags.ts#L79): A utility type that applies constraint, format, or transformation tags to properties of deeply nested or external types using dot-separated path mappings.
|
|
122
|
+
- [ResolveDefaults](file:///Users/tomaskorenko/Projects/Github/webergency-utils/typechecker/src/runtime/tags.ts#L87): A helper type that removes the optional flag (`?`) from properties that have defined default values.
|
|
123
|
+
- [convertPropertyCasing](file:///Users/tomaskorenko/Projects/Github/webergency-utils/typechecker/src/runtime/casing.ts#L190): A runtime utility to recursively change the casing of object keys.
|
|
124
|
+
- [toZodIssues](file:///Users/tomaskorenko/Projects/Github/webergency-utils/typechecker/src/runtime/validators.ts#L991): Utility to transform internal typechecker validation errors into Zod-compatible issue structures.
|
|
125
|
+
- [ZodLikeError](file:///Users/tomaskorenko/Projects/Github/webergency-utils/typechecker/src/runtime/validators.ts#L1017): Error class wrapping validation errors in a structure compatible with libraries expecting Zod errors.
|
|
92
126
|
|
|
93
|
-
|
|
94
|
-
export type ValidationMode = 'strict' | 'relaxed' | 'strip';
|
|
127
|
+
---
|
|
95
128
|
|
|
96
|
-
|
|
97
|
-
mode?: ValidationMode; // default: 'strict'
|
|
98
|
-
tryConvert?: boolean; // Converts string numbers, booleans, and dates (ideal for query parameters)
|
|
99
|
-
wrapArrays?: boolean; // Wraps a single value into an array if the type expects an array
|
|
100
|
-
}
|
|
101
|
-
```
|
|
129
|
+
## API Reference
|
|
102
130
|
|
|
103
|
-
|
|
104
|
-
```typescript
|
|
105
|
-
// Relaxed Mode (ignores additional properties)
|
|
106
|
-
const user = assert<User>(data, 'relaxed');
|
|
107
|
-
|
|
108
|
-
// Strip Mode (strips out any unknown properties from returned object)
|
|
109
|
-
const cleanUser = assert<User>(data, 'strip');
|
|
110
|
-
|
|
111
|
-
// Query-String Coercion
|
|
112
|
-
const query = assert<SearchQuery>(rawQuery, {
|
|
113
|
-
mode: 'strip',
|
|
114
|
-
tryConvert: true, // Coerces "18" -> 18, "true" -> true, etc.
|
|
115
|
-
wrapArrays: true // Coerces "tag" -> ["tag"] if tags: string[] is expected
|
|
116
|
-
});
|
|
117
|
-
```
|
|
131
|
+
### Validation Functions
|
|
118
132
|
|
|
119
|
-
|
|
133
|
+
#### `validate<T>(input: unknown, options?: ValidationMode | ValidationOptions): IValidation<ResolveDefaults<T>>`
|
|
120
134
|
|
|
121
|
-
|
|
135
|
+
Validates input data against type `T` and returns a structured validation result.
|
|
122
136
|
|
|
123
|
-
|
|
137
|
+
- **Parameters**:
|
|
138
|
+
- `input`: The value to validate.
|
|
139
|
+
- `options` (optional): Either a `ValidationMode` string ('strict' | 'relaxed' | 'strip') or a `ValidationOptions` object.
|
|
140
|
+
- **Returns**: `IValidation<ResolveDefaults<T>>` containing validation status, converted/stripped data, and error details.
|
|
141
|
+
- **Example**:
|
|
142
|
+
```typescript
|
|
143
|
+
const result = validate<User>(data, 'strip');
|
|
144
|
+
```
|
|
124
145
|
|
|
125
|
-
|
|
146
|
+
#### `is<T>(input: unknown, options?: ValidationMode | ValidationOptions): input is ResolveDefaults<T>`
|
|
126
147
|
|
|
127
|
-
|
|
128
|
-
import { validate, groupErrorsByPath, Minimum } from '@webergency-utils/typechecker';
|
|
148
|
+
A type guard function checking if the input matches type `T`.
|
|
129
149
|
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
150
|
+
- **Parameters**:
|
|
151
|
+
- `input`: The value to check.
|
|
152
|
+
- `options` (optional): Either a `ValidationMode` string or a `ValidationOptions` object.
|
|
153
|
+
- **Returns**: `boolean` (`true` if valid, `false` otherwise). Narrows type of `input` to `ResolveDefaults<T>` on success.
|
|
154
|
+
- **Example**:
|
|
155
|
+
```typescript
|
|
156
|
+
if (is<User>(data)) {
|
|
157
|
+
console.log(data.name);
|
|
158
|
+
}
|
|
159
|
+
```
|
|
136
160
|
|
|
137
|
-
|
|
138
|
-
id: 123, // Error: expected string, got number
|
|
139
|
-
role: "guest", // Error: literal union mismatch
|
|
140
|
-
age: 15, // Error: minimum constraint failed
|
|
141
|
-
metadata: { } // Error: complex union mismatch
|
|
142
|
-
};
|
|
161
|
+
#### `assert<T>(input: unknown, options?: ValidationMode | ValidationOptions): ResolveDefaults<T>`
|
|
143
162
|
|
|
144
|
-
|
|
145
|
-
if (!result.success) {
|
|
146
|
-
const grouped = groupErrorsByPath(result.errors);
|
|
147
|
-
console.log(JSON.stringify(grouped, null, 2));
|
|
148
|
-
}
|
|
149
|
-
```
|
|
163
|
+
Validates input data and returns it, throwing a validation error on failure.
|
|
150
164
|
|
|
151
|
-
**
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
},
|
|
177
|
-
"metadata.priority": {
|
|
178
|
-
"value": undefined,
|
|
179
|
-
"errors": ["Type<number>"]
|
|
180
|
-
}
|
|
181
|
-
}
|
|
182
|
-
```
|
|
165
|
+
- **Parameters**:
|
|
166
|
+
- `input`: The value to validate.
|
|
167
|
+
- `options` (optional): Either a `ValidationMode` string or a `ValidationOptions` object.
|
|
168
|
+
- **Returns**: `ResolveDefaults<T>` (the validated value with defaults resolved).
|
|
169
|
+
- **Throws**: `Error` containing a list of path and constraint failures, or a custom error via `options.errorFactory`.
|
|
170
|
+
- **Example**:
|
|
171
|
+
```typescript
|
|
172
|
+
const user = assert<User>(data);
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
#### `assertGuard<T>(input: unknown, options?: ValidationMode | ValidationOptions): asserts input is ResolveDefaults<T>`
|
|
176
|
+
|
|
177
|
+
An assertion guard that throws a validation error if the input does not match type `T`.
|
|
178
|
+
|
|
179
|
+
- **Parameters**:
|
|
180
|
+
- `input`: The value to check.
|
|
181
|
+
- `options` (optional): Either a `ValidationMode` string or a `ValidationOptions` object.
|
|
182
|
+
- **Returns**: `void`. Narrows the type of `input` in the enclosing scope on success.
|
|
183
|
+
- **Throws**: `Error` if validation fails.
|
|
184
|
+
- **Example**:
|
|
185
|
+
```typescript
|
|
186
|
+
assertGuard<User>(data);
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
#### `jsonSchema<T>(): any`
|
|
183
190
|
|
|
184
|
-
|
|
191
|
+
Generates a raw JSON Schema draft-07 object matching type `T` at compile time.
|
|
192
|
+
|
|
193
|
+
- **Returns**: `any` (a JSON Schema object).
|
|
194
|
+
- **Example**:
|
|
195
|
+
```typescript
|
|
196
|
+
const userSchema = jsonSchema<User>();
|
|
197
|
+
```
|
|
185
198
|
|
|
186
199
|
---
|
|
187
200
|
|
|
188
|
-
###
|
|
201
|
+
### Utility Functions and Classes
|
|
189
202
|
|
|
190
|
-
|
|
203
|
+
#### `convertPropertyCasing<T, C extends CasingFormat>(obj: T, casing: C, options?: ConvertCasingOptions): ConvertPropertyCasing<T, C>`
|
|
191
204
|
|
|
192
|
-
|
|
193
|
-
import { WithModifiers, constraint, format } from '@webergency-utils/typechecker';
|
|
194
|
-
|
|
195
|
-
// External type we cannot edit directly
|
|
196
|
-
interface ExternalUser {
|
|
197
|
-
id: string;
|
|
198
|
-
profile?: {
|
|
199
|
-
details: {
|
|
200
|
-
email: string;
|
|
201
|
-
password: string;
|
|
202
|
-
}
|
|
203
|
-
}
|
|
204
|
-
}
|
|
205
|
+
Recursively converts all property keys of an object to the specified casing format.
|
|
205
206
|
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
'
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
207
|
+
- **Parameters**:
|
|
208
|
+
- `obj`: The source object.
|
|
209
|
+
- `casing`: A `CasingFormat` string value (`'snake_case' | 'SNAKE_CASE' | 'camelCase' | 'camelCaseID' | 'PascalCase' | 'PascalCaseID' | 'kebab-case' | 'dot.case'`).
|
|
210
|
+
- `options` (optional): `ConvertCasingOptions` object.
|
|
211
|
+
- **Returns**: The casing-converted object with updated TypeScript property keys.
|
|
212
|
+
- **Example**:
|
|
213
|
+
```typescript
|
|
214
|
+
const apiResponse = convertPropertyCasing(user, 'camelCase');
|
|
215
|
+
```
|
|
212
216
|
|
|
213
|
-
|
|
217
|
+
#### `toZodIssues(errors: IValidationError[]): any[]`
|
|
214
218
|
|
|
215
|
-
|
|
219
|
+
Converts internal validation errors into Zod-compatible issues.
|
|
216
220
|
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
interface DBConfig {
|
|
221
|
-
ssl?: boolean;
|
|
222
|
-
cert?: string;
|
|
223
|
-
auth?: {
|
|
224
|
-
username?: string;
|
|
225
|
-
password?: string;
|
|
226
|
-
}
|
|
227
|
-
}
|
|
221
|
+
- **Parameters**:
|
|
222
|
+
- `errors`: Array of `IValidationError`.
|
|
223
|
+
- **Returns**: An array of Zod-like issues.
|
|
228
224
|
|
|
229
|
-
|
|
230
|
-
// Absolute check: if "ssl" is defined, "cert" must exist at root
|
|
231
|
-
'ssl': constraint.Requires<'cert'>;
|
|
225
|
+
#### `class ZodLikeError extends Error`
|
|
232
226
|
|
|
233
|
-
|
|
234
|
-
'auth.password': constraint.Requires<'.username'>;
|
|
235
|
-
}>;
|
|
236
|
-
```
|
|
227
|
+
An error class wrapper that transforms internal validation errors into a Zod-like error structure.
|
|
237
228
|
|
|
238
|
-
|
|
229
|
+
- **Constructor**: `constructor(errors: IValidationError[])`
|
|
230
|
+
- **Properties**:
|
|
231
|
+
- `name`: `'ZodError'`
|
|
232
|
+
- `issues`: Zod-like issue array.
|
|
239
233
|
|
|
240
|
-
|
|
234
|
+
---
|
|
241
235
|
|
|
242
|
-
|
|
243
|
-
import { validate, constraint, Message } from '@webergency-utils/typechecker';
|
|
236
|
+
### Interfaces and Types
|
|
244
237
|
|
|
245
|
-
interface
|
|
246
|
-
// Direct message in constraint tag
|
|
247
|
-
age: number & constraint.Minimum<18, "Must be 18 or older">;
|
|
238
|
+
#### `interface ValidationOptions`
|
|
248
239
|
|
|
249
|
-
|
|
250
|
-
email: string & constraint.Format<'email'> & Message<"Please supply a valid email address">;
|
|
251
|
-
}
|
|
252
|
-
```
|
|
240
|
+
Configuration options to customize validator behavior.
|
|
253
241
|
|
|
254
|
-
|
|
242
|
+
| Property | Type | Default | Description |
|
|
243
|
+
| :--- | :--- | :--- | :--- |
|
|
244
|
+
| `mode` | `'strict' \| 'relaxed' \| 'strip'` | `'strict'` | Validation mode strategy (strict key checking, relaxed, or key stripping). |
|
|
245
|
+
| `tryConvert` | `boolean` | `false` | Coerces compatible input primitives (e.g. numeric strings to numbers, ISO strings to Date). |
|
|
246
|
+
| `wrapArrays` | `boolean` | `false` | Wraps non-array values into single-element arrays if an array is expected. |
|
|
247
|
+
| `schema` | `any` | `undefined` | Custom JSON Schema instance. |
|
|
248
|
+
| `errorFactory` | `(errors: IValidationError[]) => Error` | `undefined` | Custom error factory for `assert` throwing. |
|
|
255
249
|
|
|
256
|
-
|
|
250
|
+
#### `interface IValidation<T>`
|
|
257
251
|
|
|
258
|
-
|
|
259
|
-
import { validate, toZodIssues, ZodLikeError } from '@webergency-utils/typechecker';
|
|
252
|
+
Result object returned by `validate`.
|
|
260
253
|
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
const zodIssues = toZodIssues(result.errors);
|
|
265
|
-
console.log(zodIssues); // [{ code: "custom", path: ["age"], message: "Minimum<18>", received: 15 }]
|
|
254
|
+
- `success`: `boolean`
|
|
255
|
+
- `data?`: `T`
|
|
256
|
+
- `errors?`: `IValidationError[]`
|
|
266
257
|
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
258
|
+
#### `interface IValidationError`
|
|
259
|
+
|
|
260
|
+
Details of a validation check failure.
|
|
261
|
+
|
|
262
|
+
- `path`: `string`
|
|
263
|
+
- `value`: `any`
|
|
264
|
+
- `error`: `string` (the constraint description or custom message)
|
|
265
|
+
|
|
266
|
+
#### `type WithModifiers<T, M>`
|
|
267
|
+
|
|
268
|
+
Applies constraint, format, or transformation tags to properties of type `T` using a path mapping `M`.
|
|
269
|
+
|
|
270
|
+
- **Generics**:
|
|
271
|
+
- `T`: The baseline type to wrap.
|
|
272
|
+
- `M`: A key-value map where keys are dot-separated paths (e.g., `'profile.email'`) and values are tags.
|
|
271
273
|
|
|
272
274
|
---
|
|
273
275
|
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
- `format.
|
|
304
|
-
- `format.
|
|
305
|
-
- `format.
|
|
306
|
-
- `format.
|
|
307
|
-
- `format.
|
|
308
|
-
- `format.
|
|
276
|
+
### Tags & Modifiers
|
|
277
|
+
|
|
278
|
+
#### `constraint` Namespace
|
|
279
|
+
|
|
280
|
+
Used to apply value constraints to types.
|
|
281
|
+
|
|
282
|
+
| Tag | Description |
|
|
283
|
+
| :--- | :--- |
|
|
284
|
+
| `constraint.MinLength<N, Msg?>` | Restricts string length to $\ge N$. |
|
|
285
|
+
| `constraint.MaxLength<N, Msg?>` | Restricts string length to $\le N$. |
|
|
286
|
+
| `constraint.Length<Min, Max>` | Shorthand for `MinLength<Min> & MaxLength<Max>`. |
|
|
287
|
+
| `constraint.Pattern<Regex, Msg?>` | Validates string using regular expression `Regex`. |
|
|
288
|
+
| `constraint.Minimum<N, Msg?>` | Inclusive minimum value restriction for `number | bigint`. |
|
|
289
|
+
| `constraint.Maximum<N, Msg?>` | Inclusive maximum value restriction for `number | bigint`. |
|
|
290
|
+
| `constraint.Range<Min, Max>` | Shorthand for `Minimum<Min> & Maximum<Max>`. |
|
|
291
|
+
| `constraint.ExclusiveMinimum<N, Msg?>` | Exclusive minimum value restriction for `number | bigint`. |
|
|
292
|
+
| `constraint.ExclusiveMaximum<N, Msg?>` | Exclusive maximum value restriction for `number | bigint`. |
|
|
293
|
+
| `constraint.MultipleOf<N, Msg?>` | Restricts `number | bigint` to multiples of `N`. |
|
|
294
|
+
| `constraint.MinItems<N, Msg?>` | Restricts array length to $\ge N$. |
|
|
295
|
+
| `constraint.MaxItems<N, Msg?>` | Restricts array length to $\le N$. |
|
|
296
|
+
| `constraint.UniqueItems<Msg?>` | Restricts arrays to deeply unique items. |
|
|
297
|
+
| `constraint.Custom<Fn, Msg?>` | Runs a custom validation function: `(val, ctx) => boolean`. |
|
|
298
|
+
| `constraint.Requires<Path | [Paths], Msg?>` | Enforces that other object property paths exist. |
|
|
299
|
+
| `constraint.Message<Msg>` | Fallback custom error message. |
|
|
300
|
+
|
|
301
|
+
#### `format` Namespace
|
|
302
|
+
|
|
303
|
+
Standard formats for string primitives.
|
|
304
|
+
|
|
305
|
+
- `format.Email`: RFC-5322 email.
|
|
306
|
+
- `format.UUID`: UUID (v1-v5).
|
|
307
|
+
- `format.URL`: HTTP/HTTPS/FTP URLs.
|
|
308
|
+
- `format.IPv4` / `format.IPv6`: IP addresses.
|
|
309
|
+
- `format.Date`: ISO date `YYYY-MM-DD`.
|
|
310
|
+
- `format.DateTime`: ISO date-time.
|
|
311
|
+
- `format.ObjectId`: MongoDB 24-character hex ObjectId.
|
|
312
|
+
- `format.Duration`: ISO-8601 duration.
|
|
313
|
+
- `format.Time`: Time string `HH:MM:SS`.
|
|
314
|
+
- `format.Byte`: Base64 string.
|
|
315
|
+
- `format.Password`: Any string (always valid placeholder).
|
|
309
316
|
- `format.Regex`: Valid regular expression string.
|
|
310
|
-
- `format.Hostname`: Valid domain name
|
|
317
|
+
- `format.Hostname`: Valid domain name.
|
|
311
318
|
- `format.URI`: Valid URI.
|
|
312
319
|
|
|
313
|
-
|
|
314
|
-
Coerce or sanitize values before validation:
|
|
320
|
+
#### `transform` Namespace
|
|
315
321
|
|
|
316
|
-
|
|
322
|
+
Sanitizes and converts input values during validation.
|
|
323
|
+
|
|
324
|
+
- `transform.Trim`: Trims string whitespace.
|
|
317
325
|
- `transform.LowerCase`: Converts string to lowercase.
|
|
318
326
|
- `transform.UpperCase`: Converts string to uppercase.
|
|
319
|
-
- `transform.Capitalize`: Capitalizes the first letter
|
|
327
|
+
- `transform.Capitalize`: Capitalizes the first letter.
|
|
320
328
|
- `transform.ToNumber`: Coerces inputs to numbers.
|
|
321
329
|
- `transform.ToBoolean`: Coerces inputs to booleans.
|
|
322
|
-
- `transform.ToDate`: Coerces
|
|
323
|
-
- `transform.Custom<Fn>`:
|
|
324
|
-
|
|
325
|
-
### 4. `tag` Namespace
|
|
326
|
-
Define defaults for optional fields:
|
|
327
|
-
|
|
328
|
-
- `tag.Default<Value>`: Injects the specified `Value` if the property is `undefined` at validation time.
|
|
329
|
-
|
|
330
|
-
### Assignability & compile-time constants
|
|
331
|
-
|
|
332
|
-
Constraint tags use **optional** phantom properties (same pattern as `tag.Default`), so plain values assign to tagged types:
|
|
330
|
+
- `transform.ToDate`: Coerces inputs to Date objects.
|
|
331
|
+
- `transform.Custom<Fn>`: Custom mapping function: `(val) => any`.
|
|
333
332
|
|
|
334
|
-
|
|
335
|
-
const age: number & Minimum<18> = 18; // OK
|
|
336
|
-
const plain: { age: number } = { age: 5 };
|
|
337
|
-
const tagged: { age: number & Minimum<18> } = plain; // OK
|
|
338
|
-
```
|
|
339
|
-
|
|
340
|
-
When the TypeScript plugin is enabled, **compile-time constants** that violate constraints produce diagnostics:
|
|
341
|
-
|
|
342
|
-
```typescript
|
|
343
|
-
const tooYoung: number & Minimum<18> = 5; // Error: does not satisfy Minimum<18>
|
|
344
|
-
const short: string & MinLength<3> = 'ab'; // Error: does not satisfy MinLength<3>
|
|
345
|
-
const empty: string[] & MinItems<1> = []; // Error: does not satisfy MinItems<1>
|
|
346
|
-
const dupes: number[] & UniqueItems = [1, 1]; // Error: does not satisfy UniqueItems
|
|
347
|
-
```
|
|
333
|
+
#### `tag` Namespace
|
|
348
334
|
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
#### Example:
|
|
352
|
-
```typescript
|
|
353
|
-
import { MinLength, Minimum, Format, UniqueItems, tag, transform, constraint } from '@webergency-utils/typechecker';
|
|
354
|
-
|
|
355
|
-
interface Profile {
|
|
356
|
-
email: string & Format<'email'> & transform.Trim & transform.LowerCase;
|
|
357
|
-
password: string & MinLength<8>;
|
|
358
|
-
age: number & Minimum<18> & constraint.Message<"Must be 18+">;
|
|
359
|
-
luckyNumbers: number[] & UniqueItems;
|
|
360
|
-
role: string & tag.Default<"user">;
|
|
361
|
-
}
|
|
362
|
-
```
|
|
335
|
+
- `tag.Default<Value>`: Injects `Value` when a property is undefined. Removes the optional modifier (`?`) when resolved with `ResolveDefaults<T>`.
|
|
363
336
|
|
|
364
337
|
---
|
|
365
338
|
|
|
366
|
-
##
|
|
339
|
+
## Troubleshooting
|
|
367
340
|
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
341
|
+
### `validate`, `is`, or `assert` calls return empty results or throw at runtime
|
|
342
|
+
- **Cause**: The compiler transformer did not execute during build.
|
|
343
|
+
- **Diagnostics Check**: Inspect your built `.js` code. If the output still contains `validate<User>(data)` or other generic validation calls as functions, compilation was bypassed.
|
|
344
|
+
- **Fix**:
|
|
345
|
+
1. Verify `npx ts-patch install` was executed successfully.
|
|
346
|
+
2. Verify the transformer plugin is registered in `tsconfig.json`.
|
|
347
|
+
3. Ensure your bundler or compiler CLI compiles using patched `tsc`.
|
|
372
348
|
|
|
373
349
|
---
|
|
374
350
|
|
|
375
|
-
##
|
|
351
|
+
## Maintenance
|
|
352
|
+
|
|
353
|
+
This package is actively maintained.
|
|
376
354
|
|
|
377
|
-
|
|
355
|
+
Bug reports and pull requests are welcome. Security issues and critical
|
|
356
|
+
regressions are prioritized. New features are considered when they align
|
|
357
|
+
with the package's existing scope.
|
|
@@ -168,7 +168,7 @@ export function evaluateStaticConstraints(constant, constraints) {
|
|
|
168
168
|
else if (c.type === 'multipleOf' && constant.kind === 'number') {
|
|
169
169
|
const v = constant.value;
|
|
170
170
|
const n = c.value;
|
|
171
|
-
let ok
|
|
171
|
+
let ok;
|
|
172
172
|
if (typeof v === 'bigint' || typeof n === 'bigint') {
|
|
173
173
|
ok = BigInt(v) % BigInt(n) === 0n;
|
|
174
174
|
}
|
package/dist/plugin.d.ts
ADDED
package/dist/plugin.js
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { collectStaticConstraintDiagnostics } from './engine/staticAsserts.js';
|
|
2
|
+
function init(modules) {
|
|
3
|
+
function create(info) {
|
|
4
|
+
const proxy = Object.create(null);
|
|
5
|
+
const ls = info.languageService;
|
|
6
|
+
for (const key of Object.keys(ls)) {
|
|
7
|
+
proxy[key] = ls[key];
|
|
8
|
+
}
|
|
9
|
+
proxy.getSemanticDiagnostics = (fileName) => {
|
|
10
|
+
const base = ls.getSemanticDiagnostics(fileName);
|
|
11
|
+
const program = ls.getProgram();
|
|
12
|
+
if (!program) {
|
|
13
|
+
return base;
|
|
14
|
+
}
|
|
15
|
+
const sourceFile = program.getSourceFile(fileName);
|
|
16
|
+
if (!sourceFile || sourceFile.isDeclarationFile) {
|
|
17
|
+
return base;
|
|
18
|
+
}
|
|
19
|
+
if (fileName.includes('node_modules')) {
|
|
20
|
+
return base;
|
|
21
|
+
}
|
|
22
|
+
const checker = program.getTypeChecker();
|
|
23
|
+
const extra = collectStaticConstraintDiagnostics(sourceFile, checker);
|
|
24
|
+
return [...base, ...extra];
|
|
25
|
+
};
|
|
26
|
+
return proxy;
|
|
27
|
+
}
|
|
28
|
+
return { create };
|
|
29
|
+
}
|
|
30
|
+
export default init;
|
package/dist/runtime/casing.d.ts
CHANGED
|
@@ -12,7 +12,7 @@ type SnakeToPascal<S extends string> = Capitalize<SnakeToCamel<S>>;
|
|
|
12
12
|
type SnakeToKebab<S extends string> = S extends `${infer L}_${infer R}` ? `${L}-${SnakeToKebab<R>}` : S;
|
|
13
13
|
type SnakeToDot<S extends string> = S extends `${infer L}_${infer R}` ? `${L}.${SnakeToDot<R>}` : S;
|
|
14
14
|
type SnakeToUpperSnake<S extends string> = Uppercase<S>;
|
|
15
|
-
type ReplaceId<S extends string> = S extends `${infer L}Id` ? `${L}ID` : S extends
|
|
15
|
+
type ReplaceId<S extends string> = S extends `${infer L}Id` ? `${L}ID` : S extends 'id' ? 'ID' : S extends 'Id' ? 'ID' : S;
|
|
16
16
|
type ToCamelCaseID<S extends string> = ReplaceId<SnakeToCamel<S>>;
|
|
17
17
|
type ToPascalCaseID<S extends string> = ReplaceId<SnakeToPascal<S>>;
|
|
18
18
|
type Leading<S extends string> = S extends `_${infer R}` ? `_${Leading<R>}` : S extends `-${infer R}` ? `-${Leading<R>}` : S extends `.${infer R}` ? `.${Leading<R>}` : S extends `$${infer R}` ? `$${Leading<R>}` : '';
|
|
@@ -20,9 +20,9 @@ type Trailing<S extends string> = S extends `${infer L}_` ? `${Trailing<L>}_` :
|
|
|
20
20
|
type StripLeading<S extends string> = S extends `_${infer R}` ? StripLeading<R> : S extends `-${infer R}` ? StripLeading<R> : S extends `.${infer R}` ? StripLeading<R> : S extends `$${infer R}` ? StripLeading<R> : S;
|
|
21
21
|
type StripTrailing<S extends string> = S extends `${infer L}_` ? StripTrailing<L> : S extends `${infer L}-` ? StripTrailing<L> : S extends `${infer L}.` ? StripTrailing<L> : S extends `${infer L}$` ? StripTrailing<L> : S;
|
|
22
22
|
type FormatCoreCasing<S extends string, Casing extends CasingFormat> = Casing extends 'snake_case' ? Normalized<S> : Casing extends 'SNAKE_CASE' ? SnakeToUpperSnake<Normalized<S>> : Casing extends 'camelCase' ? SnakeToCamel<Normalized<S>> : Casing extends 'PascalCase' ? SnakeToPascal<Normalized<S>> : Casing extends 'kebab-case' ? SnakeToKebab<Normalized<S>> : Casing extends 'dot.case' ? SnakeToDot<Normalized<S>> : Casing extends 'camelCaseID' ? ToCamelCaseID<Normalized<S>> : Casing extends 'PascalCaseID' ? ToPascalCaseID<Normalized<S>> : S;
|
|
23
|
-
export type FormatCasing<S extends string, Casing extends CasingFormat, Options extends ConvertCasingOptions =
|
|
23
|
+
export type FormatCasing<S extends string, Casing extends CasingFormat, Options extends ConvertCasingOptions = Record<never, never>> = Options['preserveEnds'] extends false ? FormatCoreCasing<StripLeading<StripTrailing<S>>, Casing> : `${Leading<S>}${FormatCoreCasing<StripLeading<StripTrailing<S>>, Casing>}${Trailing<S>}`;
|
|
24
24
|
type ExtractArray<T> = T extends any[] ? T : never;
|
|
25
|
-
export type ConvertPropertyCasing<T, Casing extends CasingFormat, Options extends ConvertCasingOptions =
|
|
25
|
+
export type ConvertPropertyCasing<T, Casing extends CasingFormat, Options extends ConvertCasingOptions = Record<never, never>> = T extends [infer F, ...infer R] ? [ConvertPropertyCasing<F, Casing, Options>, ...ExtractArray<ConvertPropertyCasing<R, Casing, Options>>] : T extends (infer E)[] ? ConvertPropertyCasing<E, Casing, Options>[] : T extends string | number | boolean | symbol | bigint | null | undefined ? T : T extends Date | RegExp | Function | Map<any, any> | Set<any> | Promise<any> ? T : T extends object ? {
|
|
26
26
|
[K in keyof T as K extends string ? FormatCasing<K, Casing, Options> : K]: ConvertPropertyCasing<T[K], Casing, Options>;
|
|
27
27
|
} : T;
|
|
28
28
|
export declare function convertPropertyCasing<T, C extends CasingFormat>(obj: T, casing: C, options?: ConvertCasingOptions): ConvertPropertyCasing<T, C>;
|
package/dist/runtime/casing.js
CHANGED
|
@@ -2,7 +2,7 @@ function normalizeString(str) {
|
|
|
2
2
|
if (/^[A-Z0-9_]+$/.test(str)) {
|
|
3
3
|
str = str.toLowerCase();
|
|
4
4
|
}
|
|
5
|
-
str = str.replace(/[
|
|
5
|
+
str = str.replace(/[-.]/g, '_');
|
|
6
6
|
str = str.replace(/([a-z0-9])([A-Z])/g, '$1_$2').toLowerCase();
|
|
7
7
|
return str.replace(/_+/g, '_').replace(/^_|_$/g, '');
|
|
8
8
|
}
|
|
@@ -12,7 +12,7 @@ function formatCasing(str, casing, options = {}) {
|
|
|
12
12
|
let trailing = '';
|
|
13
13
|
let core = str;
|
|
14
14
|
if (preserveEnds) {
|
|
15
|
-
const match = str.match(/^([_
|
|
15
|
+
const match = str.match(/^([_\-.$]*)(.*?)([_\-.$]*)$/);
|
|
16
16
|
if (match) {
|
|
17
17
|
leading = match[1];
|
|
18
18
|
core = match[2];
|
|
@@ -20,10 +20,10 @@ function formatCasing(str, casing, options = {}) {
|
|
|
20
20
|
}
|
|
21
21
|
}
|
|
22
22
|
else {
|
|
23
|
-
core = str.replace(/^([_
|
|
23
|
+
core = str.replace(/^([_\-.$]*)/, '').replace(/([_\-.$]*)$/, '');
|
|
24
24
|
}
|
|
25
25
|
const normalized = normalizeString(core);
|
|
26
|
-
let formatted
|
|
26
|
+
let formatted;
|
|
27
27
|
if (casing === 'snake_case') {
|
|
28
28
|
formatted = normalized;
|
|
29
29
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@webergency-utils/typechecker",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.10",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "TypeScript compiler plugin for runtime validation",
|
|
6
6
|
"author": "radixxko",
|
|
@@ -24,6 +24,12 @@
|
|
|
24
24
|
"import": "./dist/transformer.js",
|
|
25
25
|
"require": "./dist/transformer.js",
|
|
26
26
|
"default": "./dist/transformer.js"
|
|
27
|
+
},
|
|
28
|
+
"./plugin": {
|
|
29
|
+
"types": "./dist/plugin.d.ts",
|
|
30
|
+
"import": "./dist/plugin.js",
|
|
31
|
+
"require": "./dist/plugin.js",
|
|
32
|
+
"default": "./dist/plugin.js"
|
|
27
33
|
}
|
|
28
34
|
},
|
|
29
35
|
"files": [
|
|
@@ -31,11 +37,11 @@
|
|
|
31
37
|
],
|
|
32
38
|
"scripts": {
|
|
33
39
|
"lint": "eslint .",
|
|
40
|
+
"test": "vitest run",
|
|
41
|
+
"coverage": "vitest run --coverage",
|
|
34
42
|
"clean": "rm -rf ./dist/* node_modules package-lock.json && npm i",
|
|
35
|
-
"commit": "node -e \"const version = require('./package.json').version.trim(); require('child_process').execSync('git add . && git commit -m \\\"Version ' + version + '\\\"');\"",
|
|
36
43
|
"build": "npm run clean && tsc && node update_imports.js",
|
|
37
|
-
"
|
|
38
|
-
"test:coverage": "vitest run --coverage",
|
|
44
|
+
"commit": "node -e \"const version = require('./package.json').version.trim(); require('child_process').execSync('git add . && git commit -m \\\"Version ' + version + '\\\"');\"",
|
|
39
45
|
"version": "npm run build && npm publish --access public && npm run commit && git push"
|
|
40
46
|
},
|
|
41
47
|
"repository": {
|
package/LICENSE
DELETED
|
@@ -1,21 +0,0 @@
|
|
|
1
|
-
MIT License
|
|
2
|
-
|
|
3
|
-
Copyright (c) 2026 webergency-utils
|
|
4
|
-
|
|
5
|
-
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
-
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
-
in the Software without restriction, including without limitation the rights
|
|
8
|
-
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
-
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
-
furnished to do so, subject to the following conditions:
|
|
11
|
-
|
|
12
|
-
The above copyright notice and this permission notice shall be included in all
|
|
13
|
-
copies or substantial portions of the Software.
|
|
14
|
-
|
|
15
|
-
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
-
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
-
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
-
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
-
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
-
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
-
SOFTWARE.
|