Typescript data validator and transformer.
Chain the API together to build up a schema validator function
const dw = require('dealwith');
const schema = dw.object().schema({
name: dw.string().matches(/\w+/),
value: dw.array().items(
dw.oneof(
dw.string(),
dw.number()
)
)
})
Give the function some data to validate
// Type: unknown
const data = dataFromUnknownSource();
const output = schema(data);
Automatically returns the result with proper typing:
if (!output.hasError) {
/*
Type of output.result = {
name: string;
value: (string | number)[];
}
*/
console.log(output.result);
}
A lot of libraries deliberately avoid transforming, but this one does the opposite. You can transform and validate at the same time.
const validateAndTransform = dw.object().schema({
name: dw.string().matches(/\w+/),
value: dw.array().items(
dw.oneof(
dw.string().toNumber(),
dw.number()
)
)
}).toCustom(v => ({
name_transformed: v.name,
value_array: v.filter(a => a > 0)
}));
const data = {
name: 'test',
value: [1,2,0, '5', 0, '9']
}
const output = validateAndTransform('', data);
if (!output.hasError) {
/*
Type of output.result = {
name_transformed: string;
value_array: number[]
};
Value of output.result = {
name_transformed: 'test',
value: [1,2,5,9]
}
*/
console.log(output.result);
}
The output of executing a schema is always the same type regardless of errors:
type ValueValidationResult<T> = {
initialValue: unknown;
hasError: true;
error:
| Record<string, ValueValidationError>
| ValueValidationError[]
| KeyedError;
} | {
initialValue: unknown;
hasError: false;
result: T;
}
This union can be narrowed easily using the hasError
property. If the hasError
property is true
, the type will be narrowed to:
{
initialValue: unknown;
hasError: true;
error:
| Record<string, ValueValidationError>
| ValueValidationError[]
| KeyedError;
}
And if the hasError
property is false
the type can be narrowed to:
{
initialValue: unknown;
hasError: false;
result: T;
}
The simplest way to narrow the type is to use an if statement, as shown in the example below.
const output = someSchema(someData);
if (output.hasError) {
// Typescript narrows the type here so that `output.result` is
// not accessible and `output.error` is.
console.log('Error', output.error);
}
else {
// Likewise, typescript narrows the type in the opposite direction
// `output.error` is not accessible here, but `output.result` is.
console.log('Success', output.result);
}
This library is built on fairly functional programming concepts. If you're not familiar with at least the concept of first class functions, the following un-functional examples may help you to understand the source better.
Note than the code provided in this section is not real code, it is an approximation of how the actual code functions, but written in an imperative style.
const schema = D.number().moreThan(5).lessThan(10)
This produces a single validator function with two value level assertions
// Various value assertions
function greaterThan (key: string,v: number) {
if (v > 5) {
return;
}
throw new KeyedError(key, `Number ${v} is not greater than 5`);
},
function lessThan (key: string, v: number) {
if (v < 10) {
return;
}
throw new KeyedError(key, `Number ${v} is not less than 10`);
},
// Final schema
const schema = function (key: string, value: unknown): ValueValidationResult<number> {
if (typeof value !== 'number') {
return {
initialValue: value,
hasError: true,
error: KeyedError(key, `Value ${value} is not a number`);,
};
}
try {
lessThan(key, value);
greaterThan(key, value);
return {
initialValue: value,
hasError: false,
result: value
}
}
catch (exception) {
return {
initialValue: value,
hasError: true,
result: exception
}
}
}
When you create chains or nests of multiple types, it will essentially create two different internal schemas, and link them together under one common wrapper:
// There is both a number and a string schema involved here
const schema = D.number().lessThan(5).toString().matches(/\d/);
This will create a single schema which maps would be equivalent to something like:
// Various value assertions
function lessThan (key: string, v: number) {
if (v < 5) {
return;
}
throw new KeyedError(key, `Number ${v} is not less than 5`);
},
function matches (key: string, v: string) {
if (v.matches(/\d/)) {
return;
}
throw new KeyedError(key, `String ${v} does not match /\d/`);
}
// Number schema
function numberSchema (key: string, value: unknown): ValueValidationResult<number> {
if (typeof value !== 'number') {
return {
initialValue: value,
hasError: true,
error: KeyedError(key, `Value ${value} is not a number`);,
};
}
try {
lessThan(key, value);
return {
initialValue: value,
hasError: false,
result: value
}
}
catch (exception) {
return {
initialValue: value,
hasError: true,
result: exception
}
}
}
// String schema
function stringSchema (key: string, value: unknown): ValueValidationResult<string> {
if (typeof value !== 'string') {
return {
initialValue: value,
hasError: true,
error: KeyedError(key, `Value ${value} is not a string`);,
};
}
try {
matches(key, value);
return {
initialValue: value,
hasError: false,
result: value
}
}
catch (exception) {
return {
initialValue: value,
hasError: true,
result: exception
}
}
}
// Final schema
const schema = function (key: string, value: unknown): ValueValidationResult<string> {
const numberResult = numberSchema(key, value);
if (numberResult.hasError) {
return numberResult;
}
const valueAsString = numberResult.result;
const stringResult = stringSchema(key, valueAsString);
return stringResult;
}
Creates a string validator schema.
import D from 'dealwith';
import { unknownData } from 'unknownData';
const schema = D.string();
const output = schema(unknownData);
if (!output.hasError) {
// output.result type: string
console.log('Output', output.result);
}
The following assertions can be used in string schemas
equals(value: string)
The equals
assertion ensures that the string value provided, matches exactly the argument to equals
const schema = D.string().equals('correct');
const a = schema('incorrect'); // hasError = true
const b = schema('correct'); // hasError = false
The not
assertion will ensure that the string value provided does not exactly match the argument to equals
const schema = D.string().not.equals('correct');
const a = schema('incorrect'); // hasError = false
const b = schema('correct'); // hasError = true
allowed(values: string[])
The allowed
assertion ensures that the string value provided, matches exactly one of the strings in the argument to allowed
const schema = D.string().allowed(['yes', 'no']);
const a = schema('yes'); // hasError = false
const b = schema('no'); // hasError = false
const c = schema('hello'); // hasError = true
const d = schema('test'); // hasError = true
The not
assertion will ensure that the string value provided does not exactly match one of the strings in the argument to allowed
const schema = D.string().not.allowed(['yes', 'no']);
const a = schema('yes'); // hasError = true
const b = schema('no'); // hasError = true
const c = schema('hello'); // hasError = false
const d = schema('test'); // hasError = false
empty()
The empty
assertion ensures that the string value provided is an empty string ''
const schema = D.string().empty();
const a = schema('incorrect'); // hasError = true
const b = schema(''); // hasError = false
The not
assertion will ensure that the string value provided is not an empty string ''
const schema = D.string().not.empty();
const a = schema('correct'); // hasError = false
const b = schema(''); // hasError = true
matches()
The matches
assertion ensures that the string value provided matches the regular expression argument
const schema = D.string().matches(/\d/);
const a = schema('number one'); // hasError = true
const b = schema('number 1'); // hasError = false
The not
assertion will ensure that the string value provided does not match the regular expression argument
const schema = D.string().not.matches(/\d/);
const a = schema('number one'); // hasError = false
const b = schema('number 1'); // hasError = true
Creates a number validator schema.
import D from 'dealwith';
import { unknownData } from 'unknownData';
const schema = D.number();
const output = schema(unknownData);
if (!output.hasError) {
// output.result type: number
console.log('Output', output.result);
}
Creates a boolean validator schema.
import D from 'dealwith';
import { unknownData } from 'unknownData';
const schema = D.boolean();
const output = schema(unknownData);
if (!output.hasError) {
// output.result type: boolean
console.log('Output', output.result);
}
Creates a null validator schema.
import D from 'dealwith';
import { unknownData } from 'unknownData';
const schema = D.null();
const output = schema(unknownData);
if (!output.hasError) {
// output.result type: null
console.log('Output', output.result);
}
Creates an undefined validator schema.
import D from 'dealwith';
import { unknownData } from 'unknownData';
const schema = D.undefined();
const output = schema(unknownData);
if (!output.hasError) {
// output.result type: undefined
console.log('Output', output.result);
}
Creates an array validator schema.
import D from 'dealwith';
import { unknownData } from 'unknownData';
const schema = D.array();
const output = schema(unknownData);
if (!output.hasError) {
// output.result type: any[]
console.log('Output', output.result);
}
Creates an object validator schema.
import D from 'dealwith';
import { unknownData } from 'unknownData';
const schema = D.object();
const output = schema(unknownData);
if (!output.hasError) {
// output.result type: object
console.log('Output', output.result);
}
Creates a union validation schema, made up of one or more seperate validators
import D from 'dealwith';
import { unknownData } from 'unknownData';
const schema = D.oneof(
D.string(),
D.number()
);
const output = schema(unknownData);
if (!output.hasError) {
// output.result type: string | number
console.log('Output', output.result);
}