Skip to main content

Usage

tip

While prose is fine and all, examples are a much better way to get a good overview of Vality.

With Vality, you only have to write each model once. You can then use it for validation and get the benefits of type checking for free.

Models

Get started with writing a model definition for your schema.

ts
import { v } from "vality";
 
const User = () =>
({
username: v.string,
} as const);
ts
import { v } from "vality";
 
const User = () =>
({
username: v.string,
} as const);

A model is really just a function that returns an object. Don't forget to return the object as const or certain features of Vality will not work correctly.

You can then use this object to define how your data should look, by specifying the properties it ought to have and their respective types. For example, to make sure that a property email is always an email, add the following property to the object: email: v.email. Don't forget that objects may be nested arbitrarily deep:

ts
const User = () =>
({
username: v.string,
contact: {
email: v.string,
phone: v.number,
discord: {
username: v.string,
discriminator: v.number,
},
},
} as const);
ts
const User = () =>
({
username: v.string,
contact: {
email: v.string,
phone: v.number,
discord: {
username: v.string,
discriminator: v.number,
},
},
} as const);

Type

The real beauty of Vality is that you write your validation model and type definition in one place. Use the Parse<T> type to extract this information and use it to define your model type definition.

All you have to do is to pass typeof Model to the Parse type and it will return a type that represents the model type definition.

ts
type UserModel = Parse<typeof User>;
type UserModel = { username: string; contact: { email: string; phone: number; discord: { username: string; discriminator: number; }; }; }
ts
type UserModel = Parse<typeof User>;
type UserModel = { username: string; contact: { email: string; phone: number; discord: { username: string; discriminator: number; }; }; }
The typeof keyword is necessary because the model itself is written in JavaScript and needs to be converted to a type first.
ts
type PersonModel = Parse<Person>;
'Person' refers to a value, but is being used as a type here. Did you mean 'typeof Person'?2749'Person' refers to a value, but is being used as a type here. Did you mean 'typeof Person'?
ts
type PersonModel = Parse<Person>;
'Person' refers to a value, but is being used as a type here. Did you mean 'typeof Person'?2749'Person' refers to a value, but is being used as a type here. Did you mean 'typeof Person'?

Guards

To specify the type of a property, Vality offers a number of guards.

Guards, such as v.string and v.number, are used to check for atomar data types, and can easily be extended to check for more complex data. See the full documentation for a list of all built-in guards.

Options

To further specify the type of a property, you can additionally pass options to guards, which is done by calling it with an object.

ts
const User = () =>
({
age: v.number({ min: 18 }), // Will only accept numbers that are at least 18
realName: v.string({ m }), // We even get autocomplete!
                          
} as const);
ts
const User = () =>
({
age: v.number({ min: 18 }), // Will only accept numbers that are at least 18
realName: v.string({ m }), // We even get autocomplete!
                          
} as const);

Options have no influence on the type of the guard when processed by Parse.

Valits

More complex data structures, such as objects or arrays, are represented by valits.

Similar to guards, there are a number of built-in valits (see the full documentation for a complete list), and they too are extendable. However, unline guards, valits take arguments that determine their type. For example, v.array takes a single guard and then checks for an array of the passed guard's type.

ts
const User = () =>
({
username: v.string,
// (additional properties omitted for brevity)
 
languages: v.array(v.string),
} as const);
 
type UserModel = Parse<typeof User>;
type UserModel = { username: string; languages: string[]; }
ts
const User = () =>
({
username: v.string,
// (additional properties omitted for brevity)
 
languages: v.array(v.string),
} as const);
 
type UserModel = Parse<typeof User>;
type UserModel = { username: string; languages: string[]; }

Options

Valits can also be refined with options, which are passed by calling them with an object after specifying the guard. Basically by calling them twice (also called currying).

ts
const User = () =>
({
luckyNumbers: v.array(v.number)({ minLength: 3, maxLength: 5 }),
} as const);
ts
const User = () =>
({
luckyNumbers: v.array(v.number)({ minLength: 3, maxLength: 5 }),
} as const);

Options have no influence on the type of the valit when processed by Parse.

Shorthands (Shorts)

Another very handy feature of Vality are shorthands for certain valits. For example, an array with only one element is treated the same way as v.array. A list of these shorts can be found in the full documentation.

ts
const User = () =>
({
// Same as v.array(v.enum(v.literal("de"), v.literal("en"), v.literal("sv"), v.literal("fr")))
languages: [["de", "en", "sv", "fr"]],
} as const);
 
type UserModel = Parse<typeof User>;
type UserModel = { languages: ("de" | "en" | "sv" | "fr")[]; }
ts
const User = () =>
({
// Same as v.array(v.enum(v.literal("de"), v.literal("en"), v.literal("sv"), v.literal("fr")))
languages: [["de", "en", "sv", "fr"]],
} as const);
 
type UserModel = Parse<typeof User>;
type UserModel = { languages: ("de" | "en" | "sv" | "fr")[]; }

Options

It is not possible to provide options to shorts. If you need to provide extra constraints, you have to pass those to the verbose version. For example, [v.number]( ... ) is not valid, rather would you have to call v.array(v.number)( ... ).

Enys

Eny is a general term for everything that Vality can deal with. This includes guards, valits and shorthands, which all can be used for valits, for example. As valits take enys as arguments, and valits themselves are enys, this allows for arbitrarily deep nesting and complex data structures.

ts
const Person = () =>
({
name: v.string,
address: {
street: v.optional(v.string),
city: v.optional(v.string),
country: v.string,
},
} as const);
 
const Manufacturer = () =>
({
name: v.string,
ceo: Person,
cars: [Car],
} as const);
 
const Car = () =>
({
manufacturer: Manufacturer,
horsepower: v.number({
min: 1,
}),
fuel: ["petrol", "diesel", "electric"],
} as const);
 
// Hover the types
declare type PersonModel = Parse<typeof Person>;
declare type ManufacturerModel = Parse<typeof Manufacturer>;
declare type CarModel = Parse<typeof Car>;
type CarModel = { manufacturer: { name: string; ceo: { name: string; address: { street: string | undefined; city: string | undefined; country: string; }; }; cars: ...[]; }; horsepower: number; fuel: "petrol" | "diesel" | "electric"; }
ts
const Person = () =>
({
name: v.string,
address: {
street: v.optional(v.string),
city: v.optional(v.string),
country: v.string,
},
} as const);
 
const Manufacturer = () =>
({
name: v.string,
ceo: Person,
cars: [Car],
} as const);
 
const Car = () =>
({
manufacturer: Manufacturer,
horsepower: v.number({
min: 1,
}),
fuel: ["petrol", "diesel", "electric"],
} as const);
 
// Hover the types
declare type PersonModel = Parse<typeof Person>;
declare type ManufacturerModel = Parse<typeof Manufacturer>;
declare type CarModel = Parse<typeof Car>;
type CarModel = { manufacturer: { name: string; ceo: { name: string; address: { street: string | undefined; city: string | undefined; country: string; }; }; cars: ...[]; }; horsepower: number; fuel: "petrol" | "diesel" | "electric"; }