Version 2, 2020-01-27
Goals and use cases
- Introduce a mechanism which will allow users to extend schema with their own rules of validation without the need to modify the core of the library. These rules are not expressible in terms of TypeScript typesystem and need to be enforced purely in the runtime.
- Allow users to transform values before validation in order to support custom formats and inputs.
- Support nominal types implemented by users.
- Allow to bail out from further validation and assume value valid or invalid.
Proposal
New higher order function refine
provided by the library:
refine(refinement);
const refinement = (value, t) => {
return value + 1; // continue validation on transformed value
return t.left(value); // bail out with error
return t.right(value); // bail out with success
return t.right(value + 1); // bail out with success and transformed value
return t.toEither(value > 10); // bail out with error or success depending on the condition
};
The result of calling refine
is a function which can (optionally) take another schema as an argument and it might be passed to schemas:
const myRefinement = refine(refinement);
const validator = pipe(string, myRefinement, date, validate);
// alternatively
// validate(date(myRefinement(string())));
Examples
const even = refine((value, t) => t.toEither(value % 2 === 0));
pipe(number, even, validate)(12); // 12
// validate(even(number()))(12); // 12
pipe(number, even, validate)(3); // ValidationError
// validate(even(number()))(3); // ValidationError
const noDuplicateItems = refine((arr, t) => {
const allUnique = arr.every((item, index) => index === arr.indexOf(item));
return allUnique ? arr : t.left(arr);
});
const uniqueStringArrayValidator = pipe(array(string), noDuplicateItems, validate);
// const uniqueStringArrayValidator = validate(noDuplicateItems(array(string())));
uniqueStringArrayValidator(['a', 'b']); // ['a', 'b']
uniqueStringArrayValidator(['a', 'b', 'a']); // ValidationError
const allowTimestamps = refine((value) => (Number.isInteger(value) ? new Date(value) : value));
pipe(allowTimestamps, date, validate)(1231231231231); // Date(2009-01-06T08:40:31.231Z)
// validate(date(allowTimestamps()))(1231231231231); // Date(2009-01-06T08:40:31.231Z)
pipe(allowTimestamps, date, validate)(Infinity); // ValidationError
// validate(date(allowTimestamps()))(Infinity); // ValidationError
const usDateString = refine((value) => {
const result = typeof value === 'string' && value.match(/(?<m>\d\d)\/(?<d>\d\d)\/(?<y>\d\d\d\d)/);
if (!result) {
return value;
}
const { y, m, d } = result.groups;
return `${y}-${m}-${d}`;
});
pipe(string, usDateString, date, validate)('01/12/2021'); // Date(2021-01-12T00:00:00.000Z)
// validate(date(usDateString(string())))('01/12/2021'); // Date(2021-01-12T00:00:00.000Z)
pipe(string, usDateString, date, validate)('14/12/2021'); // ValidationError
// validate(date(usDateString(string())))('14/12/2021'); // ValidationError
const integer = refine<Integer>((value, t) => t.toEither(Number.isInteger(value)));
pipe(number, integer, validate)(1); // Integer
// validate(integer(number()))(1); // Integer
pipe(number, integer, validate)(1.5); // ValidationError
// validate(integer(number()))(1.5); // ValidationError
const email = refine<Email>((value, t) => t.toEither(value.includes('@')));
pipe(string, email, validate)('[email protected]'); // Email '[email protected]'
// validate(email(string()))('[email protected]'); // Email '[email protected]'
pipe(string, email, validate)('just hello'); // ValidationError
// validate(email(string()))('just hello'); // ValidationError
const allowFalse = refine((value, t) => (value === false ? t.right(value) : value));
pipe(allowFalse, string, validate())(false); // false
// validate(string(allowFalse()))(false); // false
pipe(allowFalse, string, validate())(true); // ValidationError
// validate(string(allowFalse()))(true); // ValidationError
const isInObject = <T extends object>(object: T, key: keyof any): key is keyof T =>
Object.prototype.hasOwnProperty.call(object, key);
const hasProperties = (properties: string[]) =>
refine((value, t) =>
properties.every((prop) => isInObject(value, prop)) ? value : t.left(value),
);
pipe(hasProperties(['foo']), validate)({ foo: 'bar' }); // { foo: "bar" }
// validate(hasProperties(["foo"])())({ foo: "bar" }); // { foo: "bar" }
pipe(hasProperties(['foo']), validate)({ bar: 'foo' }); // ValidationError
// validate(hasProperties(["foo"])())({ bar: "foo" }); // ValidationError
const url = refine<URL>((value, t) => t.toEither(value.includes('https://')));
pipe(string, url, validate)('https://github.com/') // URL 'https://github.com/'
validate(url(string()))('https://github.com/') // URL 'https://github.com/'
pipe(string, url, validate)('abc') // ValidationError
validate(url(string()))('abc') // ValidationError
Version 1, 2020-01-26
## Goals and use cases
Goal 1
- Introduce a mechanism which will allow users to extend schema with their own rules of validation without the need to modify the core of the library. These rules are not expressible in terms of TypeScript typesystem and need to be enforced purely in the runtime.
validate(evenNumber)(12); // 12
validate(evenNumber)(3); // ValidationError
validate(noDuplicateItems)(['a', 'b']); // ['a', 'b']
validate(noDuplicateItems)(['a', 'b', 'a']); // ValidationError
Goal 2
- Allow users to transform values before validation in order to support custom formats and inputs.
validate(timestamp(date()))(1231231231231); // Date(2009-01-06T08:40:31.231Z)
validate(timestamp(date()))(Infinity); // ValidationError
validate(weirdDate(date()))('01/12/2021'); // Date(2021-01-12T00:00:00.000Z)
validate(weirdDate(date()))('14/12/2021'); // ValidationError
Goal 3
- Support nominal types implemented by users.
type Integer = Nominal<number>; // `Nominal` is provided by user
const integer = (x: unknown): x is Integer => { ?? } // shape of this function is to be designed
validate(integer())(1); // Integer
validate(integer())(1.5); // ValidationError
type Email = Nominal<string>; // `Nominal` is provided by user
const email = (x: unknown): x is Email => { ?? } // shape of this function is to be designed
validate(email())('[email protected]'); // Email
validate(email())('just hello'); // ValidationError
Goal 4
- Allow to bail out from further validation and assume value valid or invalid.
validate(allowFalse(string()))(false); // false
validate(allowFalse(string()))(true); // ValidationError
Suggestion
New higher order function refine
and refineAfter
provided by the library:
refineAfter(beforeSchema)(refinement);
refine(refinement);
const refinement = (value, t) => {
return value + 1; // continue validation on transformed value
return t.left(value); // bail out with error
return t.right(value); // bail out with success
return t.right(value + 1); // bail out with success and transformed value
return t.toEither(value > 10); // bail out with error or success depending on the condition
};
Examples
const evenNumber = refineAfter(number())((value, t) => t.toEither(value % 2 === 0));
validate(evenNumber)(12); // 12
validate(evenNumber)(3); // ValidationError
const noDuplicateItems = (arr, t) => {
const allUnique = arr.every((item, index) => index === arr.indexOf(item));
return allUnique ? arr : t.left(arr);
};
const uniqueStringArray = refineAfter(array(string()))(noDuplicateItems);
validate(uniqueStringArray)(['a', 'b']); // ['a', 'b']
validate(uniqueStringArray)(['a', 'b', 'a']); // ValidationError
const withTimestamps = refine((value) => (Number.isInteger(value) ? new Date(value) : value));
validate(withTimestamps(date()))(1231231231231); // Date(2009-01-06T08:40:31.231Z)
validate(withTimestamps(date()))(Infinity); // ValidationError
const parseUsDateString = (value: string) => {
const result = value.match(/(?<m>\d\d)\/(?<d>\d\d)\/(?<y>\d\d\d\d)/);
if (!result) {
return null;
}
const { y, m, d } = result.groups;
return `${y}-${m}-${d}`;
};
const weirdDate = refineAfter(string())((value) => parseUsDateString(value) ?? value);
validate(weirdDate(date()))('01/12/2021'); // Date(2021-01-12T00:00:00.000Z)
validate(weirdDate(date()))('14/12/2021'); // ValidationError
const integer = refineAfter<Integer>(number())((value, t) => t.toEither(Number.isInteger(value)));
validate(integer())(1); // Integer
validate(integer())(1.5); // ValidationError
const email = refineAfter<Email>(string())((value, t) => t.toEither(value.includes('@')));
validate(email())('[email protected]'); // Email
validate(email())('just hello'); // ValidationError
const allowFalse = refine((value, t) => (value === false ? t.right(value) : value));
validate(allowFalse(string()))(false); // false
validate(allowFalse(string()))(true); // ValidationError