@zerotal/validator 1.4.0 → 1.5.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/CHANGELOG.md +16 -0
- package/package.json +2 -2
- package/src/FieldRule.ts +20 -8
- package/src/FormRequest.ts +3 -1
- package/src/RuleBuilder.ts +1 -2
- package/src/types.ts +33 -4
package/CHANGELOG.md
CHANGED
|
@@ -8,6 +8,22 @@ follows the Zerotal monorepo's unified versioning.
|
|
|
8
8
|
|
|
9
9
|
## [Unreleased]
|
|
10
10
|
|
|
11
|
+
## [1.5.0] — 2026-08-15
|
|
12
|
+
|
|
13
|
+
### Fixed
|
|
14
|
+
|
|
15
|
+
- **`Infer<>` resolves `array` and `object` to their contents.** Both inferred
|
|
16
|
+
`unknown`, which is worse than it sounds: the idiomatic `data.ids ?? []` turns
|
|
17
|
+
`unknown` into `{}`, so the error the user sees is
|
|
18
|
+
`Property 'map' does not exist on type '{}'` — pointing at the array they just
|
|
19
|
+
validated, with nothing to suggest the validator is where the type was lost.
|
|
20
|
+
`r.array(r.number())` is now `number[]`, `r.object({ … })` is the shape it was
|
|
21
|
+
given, and both nest to any depth. `nullable()` widens at every level rather
|
|
22
|
+
than only the top one.
|
|
23
|
+
|
|
24
|
+
`r.object()`'s shape parameter is inferred directly instead of through a
|
|
25
|
+
`Schema` constraint, which is what makes the field types reachable at all.
|
|
26
|
+
|
|
11
27
|
## [1.0.3] — 2026-08-07
|
|
12
28
|
|
|
13
29
|
### Changed
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zerotal/validator",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.5.0",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"maturity": "stable",
|
|
6
6
|
"private": false,
|
|
@@ -29,7 +29,7 @@
|
|
|
29
29
|
"typecheck": "tsc --noEmit"
|
|
30
30
|
},
|
|
31
31
|
"dependencies": {
|
|
32
|
-
"@zerotal/core": "1.
|
|
32
|
+
"@zerotal/core": "1.5.0"
|
|
33
33
|
},
|
|
34
34
|
"devDependencies": {
|
|
35
35
|
"typescript": "^5.8.0"
|
package/src/FieldRule.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { FieldRuleDefinition,
|
|
1
|
+
import type { FieldRuleDefinition, CustomFn } from "./types.ts";
|
|
2
2
|
import type { UniqueOptions } from "./dbRules.ts";
|
|
3
3
|
|
|
4
4
|
// Narrow definition types — each class's _def has a literal 'type' field so
|
|
@@ -6,8 +6,17 @@ import type { UniqueOptions } from "./dbRules.ts";
|
|
|
6
6
|
type StringDef = Omit<FieldRuleDefinition, "type"> & { type: "string" };
|
|
7
7
|
type NumberDef = Omit<FieldRuleDefinition, "type"> & { type: "number" };
|
|
8
8
|
type BooleanDef = Omit<FieldRuleDefinition, "type"> & { type: "boolean" };
|
|
9
|
-
|
|
10
|
-
|
|
9
|
+
// array/object are the two whose inferred type depends on more than `type`, so their
|
|
10
|
+
// defs keep the item/shape definitions in the type rather than widening them to the
|
|
11
|
+
// optional `children`/`shape` on FieldRuleDefinition. Without this the item type is
|
|
12
|
+
// erased and `Infer<>` can only say `unknown` for the whole array.
|
|
13
|
+
type ArrayDef<C extends FieldRuleDefinition = FieldRuleDefinition> = Omit<
|
|
14
|
+
FieldRuleDefinition,
|
|
15
|
+
"type" | "children"
|
|
16
|
+
> & { type: "array"; children: C };
|
|
17
|
+
type ObjectDef<
|
|
18
|
+
Sh extends Record<string, FieldRuleDefinition> = Record<string, FieldRuleDefinition>,
|
|
19
|
+
> = Omit<FieldRuleDefinition, "type" | "shape"> & { type: "object"; shape: Sh };
|
|
11
20
|
|
|
12
21
|
type RuleEntry = { name: string; args: unknown[]; message?: string; fn?: CustomFn };
|
|
13
22
|
|
|
@@ -513,7 +522,7 @@ export class BooleanRule extends FieldRule {
|
|
|
513
522
|
}
|
|
514
523
|
|
|
515
524
|
export class ArrayRule<T extends FieldRule> extends FieldRule {
|
|
516
|
-
readonly _def: ArrayDef
|
|
525
|
+
readonly _def: ArrayDef<T["_def"]>;
|
|
517
526
|
|
|
518
527
|
constructor(item: T) {
|
|
519
528
|
super();
|
|
@@ -544,10 +553,13 @@ export class ArrayRule<T extends FieldRule> extends FieldRule {
|
|
|
544
553
|
}
|
|
545
554
|
}
|
|
546
555
|
|
|
547
|
-
|
|
548
|
-
|
|
556
|
+
// Generic over the *rules* passed in, not over the definitions they produce. The old
|
|
557
|
+
// `S extends Schema` had no inference site — `{ [K in keyof S]: FieldRule }` mentions
|
|
558
|
+
// S only in its keys — so S collapsed to its constraint and the shape was lost.
|
|
559
|
+
export class ObjectRule<S extends Record<string, FieldRule>> extends FieldRule {
|
|
560
|
+
readonly _def: ObjectDef<{ [K in keyof S]: S[K]["_def"] }>;
|
|
549
561
|
|
|
550
|
-
constructor(shape:
|
|
562
|
+
constructor(shape: S) {
|
|
551
563
|
super();
|
|
552
564
|
const shapeRecord: Record<string, FieldRuleDefinition> = {};
|
|
553
565
|
for (const [k, v] of Object.entries(shape)) {
|
|
@@ -559,7 +571,7 @@ export class ObjectRule<S extends Schema> extends FieldRule {
|
|
|
559
571
|
nullable: false,
|
|
560
572
|
defaultVal: undefined,
|
|
561
573
|
rules: [],
|
|
562
|
-
shape: shapeRecord,
|
|
574
|
+
shape: shapeRecord as { [K in keyof S]: S[K]["_def"] },
|
|
563
575
|
};
|
|
564
576
|
}
|
|
565
577
|
}
|
package/src/FormRequest.ts
CHANGED
|
@@ -62,6 +62,8 @@ type SessionLike = {
|
|
|
62
62
|
* const data = await StorePostRequest.validate();
|
|
63
63
|
* // data.title → string (fully typed — no casts needed)
|
|
64
64
|
*/
|
|
65
|
+
// Merged with the empty interface below — the app-augmentable seam for typed fields.
|
|
66
|
+
// eslint-disable-next-line @typescript-eslint/no-unsafe-declaration-merging
|
|
65
67
|
export abstract class FormRequest {
|
|
66
68
|
/**
|
|
67
69
|
* Access the current request's HttpContext from within instance methods.
|
|
@@ -251,5 +253,5 @@ export abstract class FormRequest {
|
|
|
251
253
|
* }
|
|
252
254
|
* }
|
|
253
255
|
*/
|
|
254
|
-
// eslint-disable-next-line @typescript-eslint/no-
|
|
256
|
+
// eslint-disable-next-line @typescript-eslint/no-unsafe-declaration-merging
|
|
255
257
|
export interface FormRequest {}
|
package/src/RuleBuilder.ts
CHANGED
|
@@ -9,7 +9,6 @@ import {
|
|
|
9
9
|
PasswordRule,
|
|
10
10
|
} from "./FieldRule.ts";
|
|
11
11
|
import type { FieldRule } from "./FieldRule.ts";
|
|
12
|
-
import type { Schema } from "./types.ts";
|
|
13
12
|
|
|
14
13
|
export class RuleBuilder {
|
|
15
14
|
/**
|
|
@@ -59,7 +58,7 @@ export class RuleBuilder {
|
|
|
59
58
|
return new ArrayRule(item);
|
|
60
59
|
}
|
|
61
60
|
|
|
62
|
-
object<S extends
|
|
61
|
+
object<S extends Record<string, FieldRule>>(shape: S): ObjectRule<S> {
|
|
63
62
|
return new ObjectRule<S>(shape);
|
|
64
63
|
}
|
|
65
64
|
}
|
package/src/types.ts
CHANGED
|
@@ -47,7 +47,38 @@ export type InferFieldType<F extends FieldRuleDefinition> = F["type"] extends "s
|
|
|
47
47
|
? boolean
|
|
48
48
|
: F["type"] extends "date"
|
|
49
49
|
? Date
|
|
50
|
-
:
|
|
50
|
+
: F["type"] extends "array"
|
|
51
|
+
? InferArrayType<F>
|
|
52
|
+
: F["type"] extends "object"
|
|
53
|
+
? InferObjectType<F>
|
|
54
|
+
: unknown;
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* `array` and `object` carry their contents on `children` / `shape`, so unlike the
|
|
58
|
+
* scalar rules their inferred type is not decided by `type` alone. `ArrayRule` and
|
|
59
|
+
* `ObjectRule` thread the item/shape definitions through those two fields (see
|
|
60
|
+
* `ArrayDef` / `ObjectDef`), which is what these two read back out.
|
|
61
|
+
*
|
|
62
|
+
* Both degrade rather than fail: a definition widened to bare `FieldRuleDefinition`
|
|
63
|
+
* has an optional `children`/`shape` that infers nothing, and falls back to the
|
|
64
|
+
* loosest honest type instead of `never`.
|
|
65
|
+
*/
|
|
66
|
+
type InferArrayType<F extends FieldRuleDefinition> = F extends {
|
|
67
|
+
children: infer C extends FieldRuleDefinition;
|
|
68
|
+
}
|
|
69
|
+
? InferField<C>[]
|
|
70
|
+
: unknown[];
|
|
71
|
+
|
|
72
|
+
type InferObjectType<F extends FieldRuleDefinition> = F extends {
|
|
73
|
+
shape: infer Sh extends Record<string, FieldRuleDefinition>;
|
|
74
|
+
}
|
|
75
|
+
? { [K in keyof Sh]: InferField<Sh[K]> }
|
|
76
|
+
: Record<string, unknown>;
|
|
77
|
+
|
|
78
|
+
/** One field's type, including its `nullable` widening. Shared by `Infer` and the nested inferers. */
|
|
79
|
+
type InferField<F extends FieldRuleDefinition> = F["nullable"] extends true
|
|
80
|
+
? InferFieldType<F> | null
|
|
81
|
+
: InferFieldType<F>;
|
|
51
82
|
|
|
52
83
|
/**
|
|
53
84
|
* Infer the validated TypeScript type from a Schema.
|
|
@@ -55,9 +86,7 @@ export type InferFieldType<F extends FieldRuleDefinition> = F["type"] extends "s
|
|
|
55
86
|
* distinction is enforced at runtime only. Trade-off acknowledged in the plan.
|
|
56
87
|
*/
|
|
57
88
|
export type Infer<S extends Schema> = {
|
|
58
|
-
[K in keyof S]: S[K]
|
|
59
|
-
? InferFieldType<S[K]> | null
|
|
60
|
-
: InferFieldType<S[K]>;
|
|
89
|
+
[K in keyof S]: InferField<S[K]>;
|
|
61
90
|
};
|
|
62
91
|
|
|
63
92
|
export type ValidationErrors = Record<string, string>;
|