A JavaScript validation library, with types!
npm install justus
JUSTUS
======
> _"justus"_ (latin, adj.): _"proper"_ or _"correct"_.
JUSTUS is a very simple library for _validating_ JavaScript objects and
properly _annotating_ them with TypeScript types.
It focuses in providing an _easy_ and _terse_ syntax to define a simple schema,
used to ensure that an object is _correct_ and from which _proper_
typing can be inferred.
* Quick Start
* Inferring Types
* Validators
* Strings
* Numbers
* Booleans
* Constants
* Any
* Arrays
* Dates
* Tuples
* Objects (yes, this is the important one!!!)
* Optionals
* Any of, all of
* Extra Validators
* Validation Options
* A (slightly more) complex example
* Generating DTS files
* Copyright Notice
* License
Quick Start
-----------
You can use JUSTUS in your projects quite simply: import, write a schema and
validate. For example:
``typescript
import { number, object, string, validate } from 'justus'
// Create a validator, validating _objects_ with a specific schema
const validator = object({
// The "foo" property in the objects to validate must be a "string"
// with a minimum length of one character
foo: string({ minLength: 1 }),
// The "bar" property in the objects to validate must be a "number"
bar: number,
// Always use as const: it correctly infers types for constants, tuples, ...
} as const)
// Use the validator to validate the object specified as the second argument
const validated = validate(validator, { foo: 'xyz', bar: 123 })
validated.foo // <-- its type will be "string"
validated.bar // <-- its type will be "number"
`
Easy, terse, ultimately very readable... And all types are inferred!
#### Shorthand syntax
The validate function (or anywhere a _validation_ is needed) can accept a
_shorthand_ inline syntax. From our example above:
`typescript
import { number, string, validate } from 'justus'
const validated = validate({
foo: string({ minLength: 1 }),
bar: number,
} as const, {
foo: 'xyz',
bar: 123,
})
`
... you get the drill! See below in each _validator_ for their shorthand syntax.
Inferring Types
---------------
It is sometimes useful to have access to the inferred types produced by the
validation function. The InferValidation type does just that:
`typescript
const myTypeValidator = object({
/ a required string, with minimum length of 1 character /
foo: string({ minLength: 1 }),
/ an optional number, defaulted to 12345 /
bar: optional(number, 12345),
/ an optional date, without default /
baz: optional(date),
} as const)
type MyValidatedType = InferValidation
// here "MyValidatedType" will have the following shape:
// {
// foo: string, // plain "string"
// bar: number, // optional _with_ a default
// baz?: Date | undefined , // optional _without_ a default
// }
`
Similarly, it might be useful to have access to a minimal type required as a
validation input. This obviously differs from the input for several reasons,
like:
* optionals with default: on input we should represent optional(type, default)type
as , but on input this should be type | undefineddate
* type conversions: the validator converts numbers or strings alwaysDate
into objects, but the input type should be Date | string | number
The InferInput helps in this case:
`typescript
const myTypeValidator = object({
/ a required string, with minimum length of 1 character /
foo: string({ minLength: 1 }),
/ an optional number, defaulted to 12345 /
bar: optional(number, 12345),
/ an optional date, without default /
baz: optional(date),
} as const)
type MyInputType = InferInput
// here "MyInputType" will have the following shape:
// {
// foo: string, // plain string
// bar?: number | undefined, // optional, as it has a default
// baz?: Date | string | number | undefined, // optional, also parsed from string, number
// }
`
String Validator
----------------
String validators are created using the string function:
`typescript
import { string } from 'justus'
const s1 = string // validates any string
const s2 = string({ minLength: 1 }) // validate non empty strings
`
#### Options
* minLength?: number: The _minimum_ length of a valid stringmaxLength?: number
* : The _maximum_ length of a valid stringpattern?: RegExp
* : A RegExp enforcing a particular pattern for a valid string
#### Branded strings
Type _branding_ can be used for string primitives. For example:
`typescript
import { string, validate } from 'justus'
type UUID = string & { __brand_uuid: never }
const uuidValidator = string
pattern: /^[a-fA-F0-9]{8}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{12}$/,
minLength: 36,
maxLength: 36,
})
const value = validate(uuidValidator, 'C274773D-1444-41E1-9D3A-9F9D584FE8B5')
value = 'foo' // <- will fail, as "foo" is a string, while "value" is a UUID`
#### Implicit branding
Sometimes it might be useful to declare the _branding_ of a string without
recurring to an external type. We can easily do so by adding the brand
property our string constraints. Following the example above:
`typescript
import { string, validate } from 'justus'
const uuidValidator = string({
pattern: /^[a-fA-F0-9]{8}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{12}$/,
minLength: 36,
maxLength: 36,
brand: 'uuid',
})
const value = validate(uuidValidator, 'C274773D-1444-41E1-9D3A-9F9D584FE8B5')
value = 'foo' // <- fail! the type of "value" is "string & __brand_uuid: never"
`
#### Shorthand syntax
The shorthand syntax for string validators is simply string. For example:
`typescript
import { object, string } from 'justus'
const validator = object({
foo: string, // yep, no parenthesis, just "string"
})
`
Number Validator
----------------
Number validators are created using the number function:
`typescript
import { number } from 'justus'
const n1 = number // validates any number
const n2 = number({ minimum: 123 }) // validate numbers 123 and greater
const n3 = number({ fromString: true }) // parse strings like "12.34" or "0x0CAFE"
`
#### Options
* multipleOf?: number: The value for which a number must be multiple of for it to be validmaximum?: number
* : The _inclusive_ maximum value for a valid numberminimum?: number
* : The _inclusive_ minimum value for a valid numberexclusiveMaximum?: number
* : The _exclusive_ maximum value for a valid numberexclusiveMinimum?: number
* : The _exclusive_ minimum value for a valid numberallowNaN?: boolean
* : Whether to allow NaN or not (default: false)fromString?: boolean
* : Whether to parse numbers from strings or not (default: false)
#### Branded numbers
Type _branding_ can be used for number primitives. For example:
`typescript
import { number, validate } from 'justus'
type Price = number & { __brand_price: never }
const priceValidator = number
multipleOf: 0.01, // cents, anyone? :-)
minimum: 0, // no negative prices, those are _discounts_
})
const value = validate(priceValidator, 123.45)
value = 432 // <- will fail, as 432 is a number, while "value" is a Price`
#### Implicit branding
Sometimes it might be useful to declare the _branding_ of a number without
recurring to an external type. We can easily do so by adding the brand
property our number constraints. Following the example above:
`typescript
import { number, validate } from 'justus'
const priceValidator = number({
multipleOf: 0.01, // cents, anyone? :-)
minimum: 0, // no negative prices, those are _discounts_
brand: 'price',
})
const value = validate(priceValidator, 123.45)
value = 432 // <- fail! the type of "value" is "number & __brand_price: never"
`
#### Shorthand syntax
The shorthand syntax for number validators is simply number. For example:
`typescript
import { number, object } from 'justus'
const validator = object({
foo: number, // yep, no parenthesis, just "number"
})
`
Boolean Validator
-----------------
The boolean validator is represented by the boolean constant:
`typescript
import { boolean, object } from 'justus'
const validator = object({
foo: boolean, // it's a constant, no options!
})
`
To validate the string true or false as booleans, simply create thefromString
validator with the option (defaults to false):
`typescript
import { boolean, object, validate } from 'justus'
const validator = object({
foo: boolean({ fromString: true }),
})
// Here myValue can be a boolean or the string "true" or "false" (case insensitive)
const bool = validate(validator, myValue)
`
Constant Validator
------------------
Cosntant validators are created using the constant function:
`typescript
import { constant } from 'justus'
const c1 = constant('foo') // validates the string constant "foo"number
const c2 = constant(12345) // validates the constant 12345boolean
const c3 = constant(false) // validates the constant falsenull
const c4 = constant(null) // validates the constant`
The constant validator requires a string, number, boolean or null
constant.
#### Shorthand syntax
The shorthand syntax for constant validators is simply its value. For example:
`typescript
import { object, validate } from 'justus'
const validator = object({
foo: 'foo', // the string "foo"
bar: 12345, // the number 12345
baz: false, // the boolean false
nil: null, // the null constant
} as const) // yep, don't forget "as const" to infer types correctly
const result = validate(validator, something)
result.foo // <- its type will be "foo" (or "string" if you didn't use "as const")12345
result.bar // <- its type will be (or "number" if you didn't use "as const")false
result.baz // <- its type will be (or "boolean" if you didn't use "as const")null
result.nil // <- its type will be (or "any" if you didn't use "as const")`
Any Validator
-------------
The _any_ validator is represented by the any constant:
`typescript
import { any, object, validate } from 'justus'
const validator = object({
foo: any // it's a constant, no options!
})
const result = validate(validator, something)
result.foo // <- its type will be any`
Array Validator
---------------
Array validators are created using the array or arrayOf functions:
`typescript
import { array, arrayOf, number, string } from 'justus'
const a1 = array // validates any array
const a2 = array({ maxItems: 10, items: string }) // array of strings
const a3 = arrayOf(number) // array of numbers
`
#### Options
* maxItems?: number: The _maximum_ number of elements a valid ArrayminItems?: number
* : The _minimum_ number of elements a valid ArrayuniqueItems?: boolean
* : Indicates if the Array's elements must be uniqueitems?: V
: The _type_ of each individual item in the Array /
#### Shorthand syntax
The shorthand syntax for string validators is simply array. For example:
`typescript
import { array, object } from 'justus'
const validator = object({
foo: array, // validate any array, of any length, containing anything
})
`
The arrayOf function can also be considered a _shorthand_ of the fullarray({ items: ... }). For example the two following declarations are
equivalent:
`typescript
import { array, arrayOf, string } from 'justus'
const a1 = array({ items: string })
const a2 = arrayOf(string) // same as above, just more readable
`
Date Validator
--------------
Date validators are created using the date function:
`typescript
import { date } from 'justus'
const d1 = date // validates any date
const d2 = date({ format: 'iso' }) // validate ISO dates
`
> NOTE: Date validators also _convert_ dates (in string format), or
> timestamps (milliseconds from the epoch) into proper Date instances.
#### Options
* format?: 'iso' | 'timestamp': The format for dates, iso for _ISO Dates_timestamp
(as outlined in RFC 3339) or for the number of milliseconds sincefrom?: Date
the epoch
* : The earliest value a date can haveuntil?: Date
* : The latest value a date can have
#### Shorthand syntax
The shorthand syntax for number validators is simply date. For example:
`typescript
import { date, object } from 'justus'
const validator = object({
foo: date, // anything that can be converted to Date will be!`
})
Tuple Validator
---------------
A _tuple_ is (by definition) _a finite ordered list (sequence) of elements_.
Tuple validators are created using the tuple function:
`typescript
import { tuple, string, number, boolean } from 'justus'
// Validates 3 elements tuple: (in order) a string, a number and a boolean
const t1 = tuple([ string, number, boolean ])
// Validates a tuple whose first element is a string, followed by zero or more
// numbers, and wholse last element is a boolean
const t2 = tuple([ string, ...number, boolean ]) // yay! rest parameters!
`
As shown above, any Validator (or one of its shorthands) can be used as a
_rest parameter_ implying zero or more elements of the specified kind.
A more complex example:
`typescript
import { number, object, string, tuple, validate } from 'justus'
const myObject = object({
version: number,
title: string,
})
// This is the silliest tuple ever written, but outlines our intentions:
const sillyTuple = tuple([ 'start', ...myObject, 'end' ] as const)
// Validate using our tuple:
validate(sillyTuple, [
'start', // yep, a constant
{ version: 1, title: 'Hello world' },
{ version: 2, title: 'Foo, bar and baz' },
'end', // the last
])
`
Object Validator
----------------
As seen in the examples above, object validators are created using the
object function:
`typescript
import { object, string, number } from 'justus'
const o1 = object // any object (excluding null - darn JavaScript)
const o2 = object({
foo: string, // any string
bar: number, // any number
baz: 'Hello, world!', // the constant "Hello, world!"
} as const)
`
#### Shorthand syntax
The shorthand syntax for object validators is simply object. For example:
`typescript
import { arrayOf, object } from 'justus'
const validator = arrayOf(object) // won't validate if the array has numbers, strings, ...
`
#### Allow additional properties
Sometimes it's necessary to allow additional properties in an object.
Destructuring ...allowAdditionalProperties in an objects does the trick!
`typescript
import { allowAdditionalProperties, boolean, number, object, string, validate } from 'justus'
const o1 = object({
foo: string, // any string
bar: number, // any number
...allowAdditionalProperties, // any other key will be "any"
})
const result1 = validate(o1, something)
result1.foo // <-- this will be a "string"
result1.bar // <-- this will be a "number"
result1.baz // <-- additional property, this will be "any"
// additional properties with a type
const o2 = object({
foo: string, // any string
bar: number, // any number
...allowAdditionalProperties(boolean), // any other key will be "boolean"
})
const result2 = validate(o2, something)
result2.foo // <-- this will be a "string"
result2.bar // <-- this will be a "number"
result2.baz // <-- additional property, this will be "boolean"
`
Here allowAdditionalProperties is also a function, which can take some
parameters to configure its behaviour:
* ...allowAdditionalProperties: default shorthand, allows additionalany
properties and will infer the type for them....allowAdditionalProperties()
* : as a function, and same as above, it allowsany
additional properties and will infer the type for them....allowAdditionalProperties(true)
* : as a function, and same as above, itany
allows additional properties and will infer the type for them....allowAdditionalProperties(false)
* : as a function, it _forbids_ any...allowAdditionalProperties(... type ...)
additional property in objects, useful when extending objects (see below)
* : as a function, it allows
additional properties in objects and ensures their type is correct
#### Extending objects
Simply destructure one into another. For example:
`typescript
import { object, string, number, boolean } from 'justus'
const o1 = object({
foo: string, // any string
bar: number, // any number
})
const o2 = object({
...o1, // anything part of "o1" will be here as well!
bar: boolean, // here "bar" is no longer a number, but a boolean
baz: number, // add the "baz" property as a number
} as const)
`
A slightly more complex scenario arises when considering additional properties
in the base object, but forcedly forbidding them in an extend one.
To do so, simply override in the extended object as follows:
Simply destructure one into another. For example:
`typescript
import { allowAdditionalProperties, boolean, number, object, string } from 'justus'
const o1 = object({
foo: string, // any string
bar: number, // any number
...allowAdditionalProperties(boolean), // any other property is a boolean
})
const o2 = object({
...o1, // anything part of "o1" will be here as well!
baz: boolean, // add "baz" to "o1", forcing it to be a "boolean"
...allowAdditionalProperties(false), // no more additional properties here!
} as const)
`
#### Ensure properties never exist
When allowing extra properties, or extending objects, we might want to validate
the _non-existance_ of a specific property. We can do this setting a property
to never.
`typescript
import { allowAdditionalProperties, never, number, object, string } from 'justus'
const o1 = object({
foo: string, // any string
bar: number, // any number
})
const o2 = object({
...o1, // anything part of "o1" will be here as well!
bar: never, // remove "bar" from the properties inherited by "o1"
} as const)
const o3 = object({
...o1, // anything part of "o1" will be here as well!
...allowAdditionalProperties, // allow additional properties as "any"
baz: never, // even with additional properties, "baz" must not exist
} as const)
`
#### Simple records
When attempting to validate a simple Record theobjectOf
function can come handy:
`typescript
import { objectOf, number } from 'justus'
const o1 = objectOf(number)
// here "o1" will have the shape "Record
const o2 = objectOf({ test: number })
// here "o2" will have the shape "Record
`
Optional Validator
------------------
Optional properties properties can also be declared in objects, arrays, ...:
`typescript
import { object, arrayOf, optional, string, number, validate } from 'justus'
const o1 = object({
foo: string, // any string, but must be a string
bar: optional(number), // optional property as "number | undefined"
})
const r1 = validate(o1, something)
// here "r1" will have a shape like "{ foo: string, bar?: number | undefined }"
const o2 = arrayOf(optional(string)) // array members will be "string | undefined"
const r2 = validate(o2, something)
// here "r2" will have a shape like "(string | undefined)[]"
`
The optional validator can _also_ be used to inject _default values_ in case
the source object doesn't have one. To do so, we can simply use the _second_
parameter of our optional(...) function:
`typescript
import { object, optional, number, validate } from 'justus'
const o1 = object({
foo: optional(number, 123), // any number, default is 123
})
const r1 = validate(o1, {})
// here "r1" will be "{ foo: 123 }" (the default value)
const r2 = validate(o1, { foo: 321 })
// here "r2" will be "{ foo: 321 }" (overrides the default value)
`
Union Validators
----------------
Unions (either _all_ or _any_) are defined using the allOf or oneOf
functions.
To make sure all validations pass use allOf:
`typescript
import { allOf, number, object, string, validate } from 'justus'
const o1 = object({ foo: string })
const o2 = object({ bar: number })
const result = validate(allOf(o1, o2), something)
// result here will have the type of what's inferred by o1 _and_ o2
result.foo // <-- this is a "string"
result.bar // <-- this is a "number"
// be careful about never!
const result2 = validate(allOf(number, string), something)
// obviously "result2" will be of type "never" as "number" and "string" do not match!
`
More useful, to make sure all validations pass use oneOf:
`typescript
import { number, oneOf, string, validate } from 'justus'
const result = validate(oneOf(number, string), something)
result // <-- its type will be "number | string"
`
Extra Validators
----------------
A number of _extra_ validators are also included, but not exported by default
in order to reduce the bundle size when Justus is bundled in client apps.
Those are:
* arn: Validate an ARN (Amazon Resource Name) as a stringimport { arn } from 'justus/extra/arn'
* parseArn
* : Validate an ARN (Amazon Resource Name) and parse its componentsimport { parseArn } from 'justus/extra/arn'
* ean13
* : Validate a standard _EAN-13_ barcode number (as a string)import { ean13 } from 'justus/extra/ean13'
* url
* : Validate a string URL and converts it into a proper URL objectimport { url } from 'justus/extra/url'
* uuid
* : Validate a string UUID plain and simpleimport { uuid } from 'justus/extra/uuid'
*
Validation Options
------------------
The validate(...) function accepts (as a third parameter) some validation
options. Those are:
* stripAdditionalProperties: the validate(...) function will ignore anystripForbiddenProperties
object property that was not declared in the schema, and will strip them
out of the returned object (rather than failing).
* : the validate(...) function will ignore anystripOptionalNulls
_forbidden_ property that was declared in the schema (for more info see
below), and will strip them out of the
returned object (rather than failing).
* : the validate(...) function will ignore _optional_null
properties with values and strip them out of the returned object.
This is convenient when validating/stripping results coming from a database
table, where columns are declared as _nullable_.
The strip(...) function is a convenience wrapper around validate(...)stripAdditionalProperties
implying and stripOptionalNulls. Therefore:
`typescript
import { validate, strip } from 'justus'
const result1 = strip(schema, object)
// is equivalent to
const result2 = validate(schema, object, {
stripAdditionalProperties: true,
stripForbiddenProperties: false,
stripOptionalNulls: true,
})
`
A complex example
-----------------
This example is not complicated at all, but it outlines the simplicity
of the syntax intended for JUSTUS.
Let's assume we have some _time series_ data, but we can expect this in a
couple of different flavors, either V1 or V2 with some subtle differences:
`typescript
import { arrayOf, date, number, object, oneOf, string, tuple, validate } from 'justus'
// Our V1 time-series tuple is simply a timestamp followed by a numeric value
const entryv1 = tuple([ date, number ] as const)
// Our V1 response from the time series database declares the version and data
const responsev1 = object({
version: 1,
entries: arrayOf(entryv1),
} as const)
// Our V2 time-series tuple is a timestamp, followed by a number and zero or
// more strings indicating some remarks on the measurements
const entryv2 = tuple([ date, number, ...string ] as const)
// Response for V2 is the same as V1, with some extra stuff
const responsev2 = object({
version: 2,
entries: arrayOf(entryv2),
average: number, // this is extra!
} as const)
// Our combined response is either V1 or V2
const response = oneOf(responsev1, responsev2)
// GO! Validate!
const result = validate(response, {})
if (result.version === 1) {
result.version // the type here will be the literal number 1
result.entries.forEach((entry) => {
entry[0] // this will be a Date instanceDate
entry[1] // this will be a "number"
// entry[2] // will generate a typescript error
})
// response.average // will generate a typescript error
} else {
result.version // the type here will be the literal number 2
result.entries.forEach((entry) => {
entry[0] // this will be a instance`
entry[1] // this will be a "number"
entry[2] // this will be a "string"
entry[999] // this too will be a "string"
})
result.average // this will be a "number""
}
Generating DTS files
--------------------
Sometimes it might be necessary to generate .d.ts files for your schemas,
rather than relying on the type inference provided by JUSTUS.
For example, if you were to use JUSTUS on a server application to validate
HTTP requests and responses, and wanted to have strong typing when interacting
with it from a client, you might not necessarily want to have JUSTUS (and your
schemas) as a client dependency.
So, assuming your schemas might look something like this:
`typescript
import { number, object, string } from 'justus'
// this will be exported as a type
const uuid = string({ brand: 'uuid ' })
// this will be embedded in product below
const price = number({ brand: 'price' })
// object mapping two validators above
const product = object({
uuid,
price,
name: string({ minLength: 1 }),
})
`
We can generate the DTS for UUID and Product (we specifically not exportProduct in this example) using our dts-generator as follows:
`typescript
import { generateTypes } from 'justus/dts-generator'
// Note how we rename the exports to "UUID" and "Product" (casing, ...)
const dts = generateTypes({
UUID: uuid,
Product: product,
})
`
The resulting dts will be a string containing the DTS as follows:
`typescript``
export type UUID = string & {
__brand_uuid : never;
};
export type Product = {
uuid: UUID;
price: number & {
__brand_price: never;
};
name: string;
};