Skip to content

Commit ab37494

Browse files
committed
fix: helper better type support & insure primitives is required
1 parent 28d5a62 commit ab37494

5 files changed

Lines changed: 132 additions & 110 deletions

File tree

README.md

Lines changed: 37 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,13 @@
11
# Tiny Schema Validator
22

3+
small, practical, and type-safe Schema validator.
4+
35
[![GitHub license](https://img.shields.io/github/license/5alidz/tiny-schema-validator)](https://github.com/5alidz/tiny-schema-validator/blob/master/LICENSE) ![Minzipped size](https://img.shields.io/bundlephobia/minzip/tiny-schema-validator.svg)
46

5-
- installation
6-
- usage
7-
- schema
8-
- validators
9-
- advanced usage
10-
- caveats
7+
## History
8+
9+
This started as a side-project for me to learn about advanced TypeScript topics and was never intended to be an npm package,
10+
but I liked how it turned up and decided that it might be useful to use in my future projects.
1111

1212
## Installation
1313

@@ -33,29 +33,18 @@ export const Person = createSchema({
3333
});
3434
```
3535

36-
and in TypeScript
36+
and in TypeScript everything is the same, but to get the data type inferred from the schema you can do this:
3737

3838
```ts
39-
import { createSchema, _ } from 'tiny-schema-validator';
40-
41-
interface IPerson {
42-
name: string;
43-
age: number;
44-
email: string;
45-
}
46-
47-
export const Person = createSchema<IPerson>({
48-
name: _.string({ maxLength: [100, 'too-long'], minLength: [2, 'too-short'] }),
49-
age: _.number({ max: [150, 'too-old'], min: [13, 'too-young']}),
50-
email: _.string({ pattern: [/^[^@]+@[^@]+\.[^@]+$/, 'invalid-email']});
51-
});
39+
// PersonType { name: string; age: number; email: string; }
40+
export type PersonType = ReturnType<typeof Person.produce>;
5241
```
5342

5443
## Schema
5544

5645
When you create a schema, you will get a nice API to handle multiple use-cases in the client and the server.
5746

58-
- `is(data: any): boolean` check if the data is valid
47+
- `is(data: any): boolean` check if the data is valid (eager evaluation)
5948
- `validate(data: any): Errors` errors returned has the same shape as the schema you defined (does not throw)
6049
- `produce(data: any): data` throws an error when the data is invalid. otherwise, it returns data
6150
- `embed(config?: { optional: boolean })` embeds the schema in other schemas
@@ -70,7 +59,7 @@ Person.is({ name: 'john', age: 42, email: '[email protected]' }); // true
7059
Person.validate({}); // { name: 'invalid-type', age: 'invalid-type', email: 'invalid-type' }
7160
Person.validate({ name: 'john', age: 42, email: '[email protected]' }); // null
7261

73-
Person.produce(undefined); // throws an error with the same shape as the schema
62+
Person.produce(undefined); // throws { name: 'invalid-type' }
7463

7564
// embedding the person schema
7665
const GroupOfPeople = createSchema({
@@ -127,26 +116,27 @@ Check out the full validators API below:
127116

128117
### Custom validators
129118

130-
You can use validators from `_` as building blocks for your custom validator:
119+
To create custom validators that does not break type inference:
120+
121+
- use validators from `_` as building blocks for your custom validator.
122+
- your custom validator should define an `optional` and `required` functions.
123+
124+
Example of creating custom validators:
131125

132126
```js
133-
const alphaNumeric = validatorOpts =>
134-
_.string({
127+
const alphaNumeric = (() => {
128+
const config = {
135129
pattern: [/^[a-zA-Z0-9]*$/, 'only-letters-and-number'],
136-
...validatorOpts,
137-
});
138-
139-
const email = validatorOpts =>
140-
_.string({
141-
pattern: [/^[^@]+@[^@]+\.[^@]+$/, 'invalid-email'],
142-
...validatorOpts,
143-
});
130+
};
131+
return {
132+
required: additional => _.string({ ...additional, ...config, optional: false }), // inferred as Required
133+
optional: additional => _.string({ ...additional, ...config, optional: true }), // inferred as Optional
134+
};
135+
})();
144136

145137
const Person = createSchema({
146138
// ...
147-
username: alphaNumeric({ maxLength: [20, 'username-too-long'] }),
148-
email: email(),
149-
alt_email: email({ optional: true }),
139+
username: alphaNumeric.required({ maxLength: [20, 'username-too-long'] }),
150140
// ...
151141
});
152142
```
@@ -169,14 +159,16 @@ const User = createSchema({
169159
});
170160

171161
const form_ui = User.traverse({
172-
number(path, key) {
162+
number({ path, key }) {
173163
if (path.includes('profile')) return { type: 'number', label: key };
174164
return null; // otherwise ignore
175165
},
176-
string(path, key) {
166+
string({ path, key }) {
177167
if (path.includes('profile')) return { type: 'text', label: key };
178168
return null; // otherwise ignore
179169
},
170+
// this is required to get the type of "profile" correct
171+
record: () => null,
180172
});
181173

182174
console.log(form_ui); /*
@@ -197,9 +189,7 @@ The return type of your visitor is important, and there are a few considerations
197189
Returning `null` from visitor signals to ignore this node from the result, with the exception:
198190
`record | recordof | list | listof`, returning `null` signals to continue down recursively.
199191

200-
So to return something from `record` visitor you will need to visit its children recursively.
201-
202-
_Note_: in most cases defining only the primitive visitors is enough.
192+
So to return something from `record` visitor for example, you will need to visit its children recursively.
203193

204194
Continuing from the previous `User` Example
205195

@@ -231,8 +221,8 @@ const customTraverse = (key, validator) => {
231221
};
232222

233223
const form_ui = User.traverse({
234-
record(path, key, recordValidator) {
235-
return customTraverse(key, recordValidator);
224+
record({ path, key, validator }) {
225+
return customTraverse(key, validator);
236226
},
237227
});
238228
```
@@ -252,3 +242,7 @@ _.list([_.number({ optional: true /* THIS IS IGNORED */ }), _.number()]);
252242
const list = createSchema({ list: _.listof(_.string()) });
253243
list.validate({ list: ['string', 42, 'string'] }); // { list: { 1: 'invalid-type' } }
254244
```
245+
246+
## Recursive types
247+
248+
Currently there's no easy way to create recursive types, if you think you could help, PRs are welcome

src/helpers.ts

Lines changed: 41 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -20,13 +20,11 @@ import {
2020
} from './validatorTypes';
2121
import { $boolean, $list, $listof, $number, $record, $recordof, $string } from './constants';
2222

23-
export function string(config: { optional: true } & Omit<StringOptions, 'type'>): O<StringOptions>;
24-
export function string(config: { optional: false } & Omit<StringOptions, 'type'>): R<StringOptions>;
25-
export function string(
26-
config: { optional?: boolean } & Omit<StringOptions, 'type'>
27-
): StringValidator;
28-
export function string(config: Omit<StringOptions, 'type'>): R<StringOptions>;
2923
export function string(): R<StringOptions>;
24+
export function string(config: Omit<StringOptions, 'type'>): R<StringOptions>;
25+
export function string(config: { optional: false } & Omit<StringOptions, 'type'>): R<StringOptions>;
26+
export function string(config: { optional: true } & Omit<StringOptions, 'type'>): O<StringOptions>;
27+
3028
export function string(
3129
config?: { optional?: boolean } & Omit<StringOptions, 'type'>
3230
): StringValidator {
@@ -37,13 +35,11 @@ export function string(
3735
};
3836
}
3937

38+
export function number(): R<NumberOptions>;
39+
export function number(config: Omit<NumberOptions, 'type'>): R<NumberOptions>;
4040
export function number(config: { optional: true } & Omit<NumberOptions, 'type'>): O<NumberOptions>;
4141
export function number(config: { optional: false } & Omit<NumberOptions, 'type'>): R<NumberOptions>;
42-
export function number(
43-
config: { optional?: boolean } & Omit<NumberOptions, 'type'>
44-
): NumberValidator;
45-
export function number(config: Omit<NumberOptions, 'type'>): R<NumberOptions>;
46-
export function number(): R<NumberOptions>;
42+
4743
export function number(
4844
config?: { optional?: boolean } & Omit<NumberOptions, 'type'>
4945
): NumberValidator {
@@ -54,46 +50,63 @@ export function number(
5450
};
5551
}
5652

53+
export function boolean(): R<BooleanOptions>;
5754
export function boolean(config: { optional: true }): O<BooleanOptions>;
5855
export function boolean(config: { optional: false }): R<BooleanOptions>;
59-
export function boolean(): R<BooleanOptions>;
56+
6057
export function boolean(config?: { optional: boolean }): BooleanValidator {
6158
return {
6259
type: $boolean,
6360
optional: !!config?.optional,
6461
};
6562
}
6663

67-
export function list<T extends Validator[]>(list: T): R<ListOptions<T>>;
68-
export function list<T extends Validator[]>(
64+
export function list<T extends R<Validator>[]>(list: T): R<ListOptions<T>>;
65+
export function list<T extends R<Validator>[]>(
6966
list: T,
7067
config: { optional: false }
7168
): R<ListOptions<T>>;
72-
export function list<T extends Validator[]>(list: T, config: { optional: true }): O<ListOptions<T>>;
73-
export function list<T extends Validator[]>(
69+
export function list<T extends R<Validator>[]>(
70+
list: T,
71+
config: { optional: true }
72+
): O<ListOptions<T>>;
73+
74+
export function list<T extends R<Validator>[]>(
7475
list: T,
7576
config?: { optional: boolean }
7677
): ListValidator<T> {
77-
return { type: $list, optional: !!config?.optional, shape: list };
78+
return {
79+
type: $list,
80+
optional: !!config?.optional,
81+
shape: list.map(v => ({ ...v, optional: false })) as T,
82+
};
7883
}
7984

80-
export function listof<T extends Validator>(v: T): R<ListofOptions<T>>;
81-
export function listof<T extends Validator>(v: T, config: { optional: false }): R<ListofOptions<T>>;
82-
export function listof<T extends Validator>(v: T, config: { optional: true }): O<ListofOptions<T>>;
83-
export function listof<T extends Validator>(
85+
export function listof<T extends R<Validator>>(v: T): R<ListofOptions<T>>;
86+
export function listof<T extends R<Validator>>(
87+
v: T,
88+
config: { optional: false }
89+
): R<ListofOptions<T>>;
90+
export function listof<T extends R<Validator>>(
91+
v: T,
92+
config: { optional: true }
93+
): O<ListofOptions<T>>;
94+
95+
export function listof<T extends R<Validator>>(
8496
v: T,
8597
config?: { optional: boolean }
8698
): ListofValidator<T> {
8799
return {
88100
type: $listof,
89101
optional: !!config?.optional,
90-
of: v,
102+
of: { ...v, optional: false },
91103
};
92104
}
93105

94106
export function record<T extends Schema>(s: T): R<RecordOptions<T>>;
95107
export function record<T extends Schema>(s: T, config: { optional: false }): R<RecordOptions<T>>;
96108
export function record<T extends Schema>(s: T, config: { optional: true }): O<RecordOptions<T>>;
109+
97110
export function record<T extends Schema>(s: T, config?: { optional: boolean }): RecordValidator<T> {
98111
return {
99112
type: $record,
@@ -102,22 +115,23 @@ export function record<T extends Schema>(s: T, config?: { optional: boolean }):
102115
};
103116
}
104117

105-
export function recordof<T extends Validator>(v: T): R<RecordofOptions<T>>;
106-
export function recordof<T extends Validator>(
118+
export function recordof<T extends R<Validator>>(v: T): R<RecordofOptions<T>>;
119+
export function recordof<T extends R<Validator>>(
107120
v: T,
108121
config: { optional: false }
109122
): R<RecordofOptions<T>>;
110-
export function recordof<T extends Validator>(
123+
export function recordof<T extends R<Validator>>(
111124
v: T,
112125
config: { optional: true }
113126
): O<RecordofOptions<T>>;
114-
export function recordof<T extends Validator>(
127+
128+
export function recordof<T extends R<Validator>>(
115129
v: T,
116130
config?: { optional: boolean }
117131
): RecordofValidator<T> {
118132
return {
119133
type: $recordof,
120-
of: v,
134+
of: { ...v, optional: false },
121135
optional: !!config?.optional,
122136
};
123137
}

test/index.test.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,10 @@ describe('createSchema throws when', () => {
1414

1515
describe('eager validation', () => {
1616
const s = createSchema({
17-
a: _.record({ b: _.string(), c: _.record({ d: _.number(), e: _.number() }) }),
17+
a: _.record({
18+
b: _.string({ optional: true }),
19+
c: _.record({ d: _.number(), e: _.number({ optional: true }) }),
20+
}),
1821
});
1922

2023
test('test 1', () => {

test/traverse.test.ts

Lines changed: 23 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,28 @@ describe('traverse', () => {
88
profile: _.record({ username: _.string(), email: _.string(), age: _.number() }),
99
});
1010

11+
test('basic test', () => {
12+
const form_ui = Person.traverse({
13+
number({ path, key }) {
14+
if (path.includes('profile')) return { type: 'number', label: key };
15+
return null; // otherwise ignore
16+
},
17+
string({ path, key }) {
18+
if (path.includes('profile')) return { type: 'text', label: key };
19+
return null; // otherwise ignore
20+
},
21+
record: () => null,
22+
});
23+
24+
expect(form_ui).toStrictEqual({
25+
profile: {
26+
username: { type: 'text', label: 'username' },
27+
email: { type: 'text', label: 'email' },
28+
age: { type: 'number', label: 'age' },
29+
},
30+
});
31+
});
32+
1133
test('custom traverse 1', () => {
1234
const customTraverse = (key: string, validator: Validator) => {
1335
if (validator.type == 'string') {
@@ -79,27 +101,7 @@ describe('traverse', () => {
79101
});
80102
});
81103

82-
test('readme example 2', () => {
83-
const profileFormInputsData = Person.traverse({
84-
number({ path, key }) {
85-
if (path.includes('profile')) return { type: 'number', label: key };
86-
return null; // otherwise ignore
87-
},
88-
string({ path, key }) {
89-
if (path.includes('profile')) return { type: 'text', label: key };
90-
return null; // otherwise ignore
91-
},
92-
record() {
93-
return 'ignore children';
94-
},
95-
});
96-
97-
expect(profileFormInputsData).toStrictEqual({
98-
profile: 'ignore children',
99-
});
100-
});
101-
102-
test('readme example', () => {
104+
test('defining only primitive visitor', () => {
103105
const profileFormInputsData = Person.traverse({
104106
number({ path, key }) {
105107
if (path.includes('profile')) return { type: 'number', label: key };

0 commit comments

Comments
 (0)