Skip to content

Commit 07a6c67

Browse files
feat: rules first typing: InferInput type and refineRules helper (#112)
* working poc * temp nor working refine * handle variants, defineRules, refineRules * chore: release v1.2.0-beta.1 * chore: test * reworked some rules * handle variants in refineRules, added tests * finished unit tests * infer state docs
1 parent 1bf719f commit 07a6c67

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

68 files changed

+1157
-249
lines changed

docs/.vitepress/config.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -92,13 +92,17 @@ const AdvancedUsage: (DefaultTheme.NavItemWithLink | DefaultTheme.NavItemChildre
9292

9393
const Typescript: (DefaultTheme.NavItemWithLink | DefaultTheme.NavItemChildren)[] = [
9494
{
95-
text: 'Typing props',
95+
text: 'Typing component props',
9696
link: '/typescript/typing-props',
9797
},
9898
{
99-
text: 'Typing rules',
99+
text: 'Rules definitions',
100100
link: '/typescript/typing-rules',
101101
},
102+
{
103+
text: 'Infer state from rules',
104+
link: '/typescript/infer-state-from-rules',
105+
},
102106
];
103107

104108
const Integrations: (DefaultTheme.NavItemWithLink | DefaultTheme.NavItemChildren)[] = [

docs/src/core-concepts/index.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,13 @@ Regle provide a list of default rules that you can use from `@regle/rules`.
104104

105105
You can find the [list of built-in rules here](/core-concepts/rules/built-in-rules)
106106

107+
108+
:::tip
109+
110+
If you prefer to have a rules-first way of typing your state (like schema libraries), you can check [how to do it here](/typescript/infer-state-from-rules)
111+
112+
:::
113+
107114
<br/>
108115

109116

docs/src/core-concepts/rules/built-in-rules.md

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,23 @@ const { r$ } = useRegle({ count: 0 }, {
107107
})
108108
```
109109
110+
## `boolean`
111+
112+
Requires a value to be a native boolean type. Mainly used for typing.
113+
114+
```ts twoslash
115+
import {useRegle, type InferInput} from '@regle/core';
116+
import {ref} from 'vue';
117+
// ---cut---
118+
import { boolean } from '@regle/rules';
119+
120+
const rules = {
121+
checkbox: { boolean },
122+
}
123+
124+
const state = ref<InferInput<typeof rules>>({});
125+
```
126+
110127
## `checked`
111128
112129
Requires a boolean value to be `true`. This is useful for checkbox inputs.
@@ -140,6 +157,24 @@ const { r$ } = useRegle({ bestLib: '' }, {
140157
})
141158
```
142159
160+
## `date`
161+
162+
Requires a value to be a native Date constructor. Mainly used for typing.
163+
164+
```ts twoslash
165+
import {useRegle, type InferInput} from '@regle/core';
166+
import {ref} from 'vue';
167+
168+
// ---cut---
169+
import { date } from '@regle/rules';
170+
171+
const rules = {
172+
birthday: { date },
173+
}
174+
175+
const state = ref<InferInput<typeof rules>>({});
176+
```
177+
143178
144179
## `dateAfter`
145180
_**Params**_
@@ -516,6 +551,24 @@ const { r$ } = useRegle({ type: '' }, {
516551
})
517552
```
518553
554+
## `number`
555+
556+
Requires a value to be a native number type. Mainly used for typing.
557+
558+
```ts twoslash
559+
import {useRegle, type InferInput} from '@regle/core';
560+
import {ref} from 'vue';
561+
562+
// ---cut---
563+
import { number } from '@regle/rules';
564+
565+
const rules = {
566+
count: { number },
567+
}
568+
569+
const state = ref<InferInput<typeof rules>>({});
570+
```
571+
519572
## `numeric`
520573
521574
Allows only numeric values (including numeric strings).
@@ -672,6 +725,41 @@ const { r$ } = useRegle({ bestLib: '' }, {
672725
})
673726
```
674727
728+
## `string`
729+
730+
Requires a value to be a native string type. Mainly used for typing
731+
732+
```ts twoslash
733+
import {useRegle, type InferInput} from '@regle/core';
734+
import {ref} from 'vue';
735+
// ---cut---
736+
import { string } from '@regle/rules';
737+
738+
const rules = {
739+
firstName: { string },
740+
}
741+
742+
const state = ref<InferInput<typeof rules>>({});
743+
```
744+
745+
## `type`
746+
747+
Define the input type of a rule. No runtime validation.
748+
Override any input type set by other rules.
749+
750+
```ts twoslash
751+
import {useRegle, type InferInput} from '@regle/core';
752+
import {ref} from 'vue';
753+
// ---cut---
754+
import { type } from '@regle/rules';
755+
756+
const rules = {
757+
firstName: { type: type<string>() },
758+
}
759+
760+
const state = ref<InferInput<typeof rules>>({});
761+
```
762+
675763
676764
## `url`
677765
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
---
2+
title: Infer state from rules
3+
description: Write your validations in the Zod way
4+
---
5+
6+
# Infer state from rules
7+
8+
Regle is state first, that means that you write your rules depending on the state structure, that's the model based way that Vuelidate introduced.
9+
10+
With Schema libraries like Zod or Valibot, it's the contrary: the state type depends on the schema output.
11+
12+
This mental model may differ to some people, the good new is Regle allow to work both ways!
13+
14+
## `InferInput`
15+
16+
`InferInput` is an utility type that can produce a object state from any rules object.
17+
18+
It will try to extract the possible state type that a rule may have, prioritizing rules that have a strict input type.
19+
20+
Some rules may have `unknown` type because it could apply to any value. To cover this, there is now type-helpers rules to help you type your state from the rules: `type`, `string`, `number`, `boolean`, `date`.
21+
22+
:::info
23+
Some types like `numeric` will feel weird as it's typed `string | number`, it's normal as the rule can also validate numeric strings. You can enforce the type by applying `number` rule to it.
24+
:::
25+
26+
```ts twoslash
27+
import {ref} from 'vue';
28+
// ---cut---
29+
import { defineRules, type InferInput} from '@regle/core';
30+
import { required, string, numeric, type } from '@regle/rules';
31+
32+
/* defineRules is not required, but it helps you catch errors in structure */
33+
const rules = defineRules({
34+
firstName: { required, string },
35+
count: { numeric },
36+
enforceType: { required, type: type<'FOO' | 'BAR'>()}
37+
})
38+
39+
type State = InferInput<typeof rules>;
40+
// ^?
41+
42+
```
43+
44+
<br/>
45+
<br/>
46+
<br/>
47+
48+
## `refineRules`
49+
50+
Regle is state first because in real world forms, rules can depend a state values.
51+
This make it a problem for dynamic rules as it would make a cyclic type error when trying to use the state inside the rules.
52+
53+
To cover this case and inspired by Zod's `refine`, Regle provides a `refineRules` helper to write dynamic rules that depend on the state, while making it possible to access a typed state.
54+
55+
56+
Anything returned by the rule refine function will override what's defined in the default rules.
57+
58+
```ts twoslash
59+
import {ref} from 'vue';
60+
// ---cut---
61+
import { refineRules, type InferInput} from '@regle/core';
62+
import { required, string, sameAs } from '@regle/rules';
63+
64+
const rules = refineRules({
65+
password: { required, string },
66+
},
67+
(state) => ({
68+
confirmPassword: { required, sameAs: sameAs(() => state.value.password) }
69+
})
70+
)
71+
72+
type State = InferInput<typeof rules>;
73+
// ^?
74+
```
75+
76+

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "regle",
3-
"version": "1.1.4",
3+
"version": "1.2.0-beta.1",
44
"private": true,
55
"description": "Headless model-based form validation library for Vue.js",
66
"scripts": {

packages/core/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@regle/core",
3-
"version": "1.1.4",
3+
"version": "1.2.0-beta.1",
44
"description": "Headless form validation library for Vue 3",
55
"scripts": {
66
"typecheck": "tsc --noEmit",

packages/core/src/core/createVariant.ts

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import {
22
computed,
33
isRef,
4+
nextTick,
45
ref,
56
toRef,
67
toValue,
@@ -15,6 +16,7 @@ import type {
1516
DeepReactiveState,
1617
JoinDiscriminatedUnions,
1718
LazyJoinDiscriminatedUnions,
19+
MaybeInput,
1820
RegleCollectionStatus,
1921
RegleFieldStatus,
2022
RegleStatus,
@@ -105,7 +107,10 @@ export function narrowVariant<
105107
root: TRoot,
106108
discriminantKey: TKey,
107109
discriminantValue: TValue
108-
): root is Extract<TRoot, { [K in TKey]: RegleFieldStatus<TValue, any, any> }> {
110+
): root is Extract<
111+
TRoot,
112+
{ [K in TKey]: RegleFieldStatus<TValue, any, any> | RegleFieldStatus<MaybeInput<TValue>, any, any> }
113+
> {
109114
return (
110115
isObject(root[discriminantKey]) &&
111116
'$value' in root[discriminantKey] &&
@@ -141,14 +146,16 @@ export function variantToRef<
141146

142147
watch(
143148
fromRoot,
144-
() => {
149+
async () => {
150+
// avoid premature load of wrong rules resulting in a false positive
151+
await nextTick();
145152
if (narrowVariant(fromRoot.value, discriminantKey, discriminantValue)) {
146153
returnedRef.value = fromRoot.value;
147154
} else {
148155
returnedRef.value = undefined;
149156
}
150157
},
151-
{ immediate: true }
158+
{ immediate: true, flush: 'pre' }
152159
);
153160

154161
return returnedRef as any;

packages/core/src/core/defaultValidators.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import type { EnumLike, Maybe, RegleRuleDefinition, RegleRuleWithParamsDefinition } from '../types';
1+
import type { EnumLike, Maybe, MaybeInput, RegleRuleDefinition, RegleRuleWithParamsDefinition } from '../types';
22

33
export interface CommonComparisonOptions {
44
/**
@@ -20,8 +20,10 @@ export type DefaultValidators = {
2020
alpha: RegleRuleWithParamsDefinition<string, [options?: CommonAlphaOptions | undefined]>;
2121
alphaNum: RegleRuleWithParamsDefinition<string | number, [options?: CommonAlphaOptions | undefined]>;
2222
between: RegleRuleWithParamsDefinition<number, [min: Maybe<number>, max: Maybe<number>]>;
23+
boolean: RegleRuleDefinition<unknown, [], false, boolean, any, unknown>;
2324
checked: RegleRuleDefinition<boolean, [], false, boolean, boolean>;
2425
contains: RegleRuleWithParamsDefinition<string, [part: Maybe<string>], false, boolean>;
26+
date: RegleRuleDefinition<unknown, [], false, boolean, MaybeInput<Date>, unknown>;
2527
dateAfter: RegleRuleWithParamsDefinition<
2628
string | Date,
2729
[after: Maybe<string | Date>, options?: CommonComparisonOptions],
@@ -86,11 +88,14 @@ export type DefaultValidators = {
8688
>;
8789
minValue: RegleRuleWithParamsDefinition<number, [count: number, options?: CommonComparisonOptions], false, boolean>;
8890
nativeEnum: RegleRuleDefinition<string | number, [enumLike: EnumLike], false, boolean, string | number>;
91+
number: RegleRuleDefinition<unknown, [], false, boolean, any, unknown>;
8992
numeric: RegleRuleDefinition<string | number, [], false, boolean, string | number>;
9093
oneOf: RegleRuleDefinition<string | number, [options: (string | number)[]], false, boolean, string | number>;
9194
regex: RegleRuleWithParamsDefinition<string, [regexp: RegExp], false, boolean>;
9295
required: RegleRuleDefinition<unknown, []>;
9396
sameAs: RegleRuleWithParamsDefinition<unknown, [target: unknown, otherName?: string], false, boolean>;
97+
string: RegleRuleDefinition<unknown, [], false, boolean, any, unknown>;
98+
type: RegleRuleDefinition<unknown, [], false, boolean, unknown, unknown>;
9499
startsWith: RegleRuleWithParamsDefinition<string, [part: Maybe<string>], false, boolean>;
95100
url: RegleRuleDefinition<string, [], false, boolean, string>;
96101
};

packages/core/src/core/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,3 +5,4 @@ export type { DefaultValidators, CommonComparisonOptions, CommonAlphaOptions } f
55
export { mergeRegles, type MergedRegles } from './mergeRegles';
66
export { createScopedUseRegle, useCollectScope, useScopedRegle } from './createScopedUseRegle';
77
export { createVariant, narrowVariant, variantToRef } from './createVariant';
8+
export { defineRules, refineRules } from './refineRules';
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
import type { Merge } from 'type-fest';
2+
import { merge } from '../../../shared';
3+
import type { InferInput, ReglePartialRuleTree, RegleUnknownRulesTree } from '../types';
4+
import type { Ref } from 'vue';
5+
6+
/**
7+
* Helper method to wrap an raw rules object
8+
*
9+
* Similar to:
10+
*
11+
* ```ts
12+
* const rules = {...} satisfies RegleUnknownRulesTree
13+
* ```
14+
*/
15+
export function defineRules<TRules extends RegleUnknownRulesTree>(rules: TRules): TRules {
16+
return rules;
17+
}
18+
19+
/**
20+
* Refine a raw rules object to set rules that depends on the state values.
21+
*
22+
* @example
23+
*
24+
* ```ts
25+
* const rules = refineRules({
26+
* password: { required, type: type<string>() },
27+
* }, (state) => {
28+
* return {
29+
* confirmPassword: { required, sameAs: sameAs(() => state.value.password)}
30+
* }
31+
* })
32+
* ```
33+
*/
34+
export function refineRules<
35+
TRules extends RegleUnknownRulesTree,
36+
TRefinement extends ReglePartialRuleTree<InferInput<TRules>> & RegleUnknownRulesTree,
37+
>(
38+
rules: TRules,
39+
refinement: (state: Ref<InferInput<TRules>>) => TRefinement
40+
): (state: Ref<InferInput<TRules>>) => Merge<TRules, TRefinement> {
41+
return (state) => merge({ ...rules }, refinement(state)) as any;
42+
}

0 commit comments

Comments
 (0)