Usage
Optimal supports individual value validation through reusable schemas, and advanced
functionality through the optimal()
function. Choose either or both APIs to
satisfy your use cases!
#
Optimal validationThe primary API for validating options objects, configuration files, databags, and many more, is
with the optimal()
function. This function accepts a
blueprint and an optional options object,
and returns a schema-like API. Internally this function is a shape, but it
provides additional functionality through options and TypeScript typings.
To understand this function, let's demonstrate it with an example. Say we have the following interface and class, and we want to validate the options within the constructor.
interface PluginOptions { id: string; debug?: boolean; priority?: 'low' | 'normal' | 'high';}
class Plugin { options: PluginOptions;
constructor(options: PluginOptions) { this.options = options; }}
We can do this by importing optimal, defining a blueprint, and calling the
.validate()
method. Let's expand upon the example above
and provide annotations for what's happening.
// Import the optimal function and any schemas we requireimport { bool, string, optimal } from 'optimal';
interface PluginOptions { // This field is required id: string; // While these 2 fields are optional debug?: boolean; priority?: 'low' | 'normal' | 'high';}
class Plugin { // We wrap the type in `Required` since optimal ensures they all exist afer validation options: Required<PluginOptions>;
constructor(options: PluginOptions) { // Instantiate optimal with a blueprint that matches the `PluginOptions` interface this.options = optimal( { // Mark this field as required to match the interface id: string().notEmpty().camelCase().required(), // Omit the required since these 2 fields are optional in the interface debug: bool(), priority: string('low').oneOf(['low', 'normal', 'high']), }, { // Provide a unique name within error messages name: this.constructor.name, }, // Immediately trigger validation and return a built object! ).validate(options); }}
Now when the plugin is instantiated, the optimal()
function will be ran to return an object in the
shape of the blueprint. For example:
// { id: 'abc', debug: false, priority: 'low' }new Plugin({ id: 'abc' }).options;
// { id: 'abc', debug: true, priority: 'low' }new Plugin({ id: 'abc', debug: true }).options;
// { id: 'abc', debug: false, priority: 'high' }new Plugin({ id: 'abc', priority: 'high' }).options;
// Throws an error for invalid `priority` valuenew Plugin({ id: 'abc', priority: 'severe' }).options;
// Throws an error for an unknown filednew Plugin({ id: 'abc', size: 123 }).options;
#
Schema validationBesides optimal()
, every schema supports a
.validate()
function that can be used to validate and
return a type casted value. View all available schemas.
import { string } from 'optimal';
const stringSchema = string();
stringSchema.validate(123); // failstringSchema.validate('abc'); // pass
// Or from the instance directly
string().notEmpty().validate(''); // fail
#
Default valuesMost schemas support a custom default value when being instantiated, which will be used as a
fallback when the field is not explicitly defined, or an explicit undefined
is passed (unless
undefinable). The default value must be the same type as the schema.
const severitySchema = string('low').oneOf(['low', 'high']);
severitySchema.validate(undefined); // -> lowseveritySchema.validate('high'); // -> high
Furthermore, the default value can also be a function that returns a value. This is useful for deferring execution, or avoiding computation heavy code. This function is passed an object path for the field being validated, the current depth object, and the entire object.
const dateSchema = date(() => new Date(2020, 1, 1));
The
func()
schema must always use the factory pattern for defining a default value, otherwise, the default function will be executed inadvertently.
#
Transforming valuesAll schemas support a chainable transform()
method that can be used to transform the value. Transformation occurs in place, and not at the
beginning or end of the validation process.
const textSchema = string() // Use smart quotes for typography .transform((value) => value.replace(/'/g, '‘').replace(/"/g, '“')) .notEmpty();
textSchema.validate(''); // throwstextSchema.validate("How's it going?"); // -> How‘s it going?
Multiple tranformations can be declared by chaining multiple
transform()
s.
#
Error messagesAll failed validations throw an error with descriptive messages for a better user experience.
However, these messages are quite generic to support all scenarios, but can be customized with a
message
object as the last argument.
// throws "String must be lower cased."string().lowerCase().validate('FOO');
// throws "Please provide a lowercased value."string().lowerCase({ message: 'Please provide a lowercased value.' }).validate('FOO');
#
Nullable fieldsMost schemas are not nullable by default, which means null
cannot be returned from a field being
validated. To accept and return null
values, chain the nullable()
method on a schema, inversely,
chain notNullable()
to not accept null
values (resets).
const objectSchema = object().notNullable(); // defaultconst nullableObjectSchema = object().nullable();
objectSchema.validate(null); // throw errornullableObjectSchema.validate(null); // -> null
When nullable, the schema's return type is
T | null
.
#
Undefinable fieldsUnlike nullable above, all schemas by default do not return undefined
, as we fallback to the
default value when undefined
is passed in. However, they are some scenarios where you want to
return undefined
, and when this happens, the default value and most validation criteria is
completely ignored.
To accept and return undefined
values, chain the undefinable()
method on a schema, inversely,
chain notUndefinable()
to not accept undefined
values (resets).
const numberSchema = number(123).notUndefinable(); // defaultconst undefinableNumberSchema = number(456).undefinable();
numberSchema.validate(undefined); // -> 123undefinableNumberSchema.validate(undefined); // -> undefined
When undefinable, the schema's return type is
T | undefined
.
#
Required & optional fieldsWhen a schema is marked as required()
, it requires the field to be explicitly defined and passed
when validating a shape, otherwise it throws an error. To invert and reset the requirement, use
the optional()
method.
const userSchema = shape({ name: string().required().notEmpty(), age: number().positive(),});
userSchema.validate({}); // throw erroruserSchema.validate({ name: 'Bruce Wayne' }); // -> (shape)
This does not change the typing and acceptance of undefined
values, it simply checks property
existence. Use undefinable fields for that functionality.
#
Logical operatorsShapes support the AND, OR, and XOR logical operators. When a schema is configured with these operators and is validated outside of a shape, they do nothing.
When and()
is chained on a schema, it requires all
related fields to be defined.
const andSchema = shape({ foo: string().and(['bar']), bar: number().and(['foo']),});
andSchema.validate({ foo: 'abc' }); // throw error
// Requires both fields to be definedandSchema.validate({ foo: 'abc', bar: 123 }); // -> (shape)
When or()
is chained, it requires 1 or more of the
fields to be defined.
const orSchema = shape({ foo: string().or(['bar', 'baz']), bar: number().or(['foo', 'baz']), baz: bool().or(['foo', 'bar']),});
orSchema.validate({}); // throw error
// Requires at least 1 field to be definedorSchema.validate({ foo: 'abc' }); // -> (shape)orSchema.validate({ bar: 123 }); // -> (shape)orSchema.validate({ foo: 'abc', baz: true }); // -> (shape)
When xor()
is chained, it requires only 1 of the
fields to be defined.
const xorSchema = shape({ foo: string().xor(['bar', 'baz']), bar: number().xor(['foo', 'baz']), baz: bool().xor(['foo', 'bar']),});
xorSchema.validate({}); // throw errorxorSchema.validate({ foo: 'abc', baz: true }); // throw error
// Requires only 1 field to be definedxorSchema.validate({ foo: 'abc' }); // -> (shape)xorSchema.validate({ bar: 123 }); // -> (shape)
#
BlueprintsA blueprint is an object of schemas that define an exact structure to be returned
from optimal()
and shape()
.
Each schema provides a default value to be used when a field of the same name is undefined or
missing.
import { Blueprint, number, string } from 'optimal';
interface User { id: number; name: string;}
const user: Blueprint<User> = { id: number().positive(), name: string().notEmpty(),};