dilav
v0.0.11
Published
A blazing fast way to transforms `unknown` types, into valid known typescript types.
Downloads
2
Maintainers
Readme
Introduction
Dilav is a blazing fast way to transforms unknown
types, into valid known typescript types. Similar to how Typescript provides type assurance at compile time, Dilav provides type assurance at run-time. Dilav is a heteropalindrome of valid.
- Introduction
- Installation
- Documentation
Why I created this project
I'm currently looking for employment and to showcase my skills I developed this project. If you like it and know of someone needing similar projects created, please contact me.
Relationship to Zod
In developing Dilav, Zod's excellent public API was leveraged, meaning Dilav provides near functional parity to Zod and it passes most of Zod's tests with minor changes. Dilav was written from scratch and shares no code with Zod, other than a few string validation regex's
Whilst the APIs are similar, Dilav includes significant changes and is therefore not a drop in replacement.
The benchmarks in the test folder shows Dilav is ~280% faster on my computer for Benchmark 1 which parses objects and arrays and 980% faster for Benchmark 2 which parses only a string and 300% faster for Benchmark 3 which only parses a string. These numbers appear high to me and I haven't yet fully explored why they are so high, so they should be taken with a pinch of salt.
Zod has ~3,950 lines of JavaScript code and Dilav has ~2,900.
Select API changes from Zod:
v.object
by default returns the original object and not a new objectv.object
by default doesn't allow unspecified propertiesz.tuple
functionality has been merged intov.array
z.discriminatedUnion
functionality was merged intov.union
z.nativeEnum
functionality was merged intov.enum
safeParse
instead of returning an object, returns an array of typeResultError
- most primitives do not need to be called -
v.string
vsz.string()
- customisation of errors is done via functions that return strings, rather than strings, providing more flexibility
- minor capitalisation and spelling changes to methods
z.ZodType
is split across multiple types.refine
andz.superRefine
are replaced by.customValidation
,.preprocess
and.postprocess
enabling customisation of input values, validations, errors and return values.describe
not implemented.brand
not implemented
Status
Dilav is in it's first alpha release, to seek feedback from people who may be interested in the project. There are a number of things that may be improved in the public API in subsequent releases. I'm also considering releasing an even more performance focused API. Dilav follows Zod's example with regards to async validations, but the API could be better so I may rework that API entirely.
Advantages of Dilav
- It's performant
- It's small
- It has no dependencies
- It should be tree shakeable allowing one to use only the parts one needs
- It's architecture is simple, meaning once stabilised it should be highly reliable
Installation
Requirements
TypeScript 5.0+!
Enable
strict
mode intsconfig.json
:{ // ... "compilerOptions": { // ... "strict": true } }
npm install
npm install dilav # npm
Basic usage
Creating a simple string schema
import { v } from 'dilav'
// creating a schema for strings
const schema = v.string
// parsing
const willParseToString = schema.parse('hello') // => "hello"
const willThrow = schema.parse(12) // => throws an error
// safe parsing doesn't throw error if validation fails
const willParseSafely = schema.safeParse('hello') // => [ undefined, "hello" ]
const willParseSafely2 = schema.safeParse(12) // => [ {input: 12, errors: ['12 is not a string']} ]
Creating an object schema
import { v } from 'dilav'
const userSchema = v.object({ username: v.string })
type UserType = v.Infer<typeof userSchema> // infers the schema type: { username: string }
const user = userSchema.parse({ username: 'Fred' }) // => { username: 'Fred' }
Documentation
Primitives
// primitive values
v.string.parse('a')
v.number.parse(1)
v.bigInt.parse(2n)
v.boolean.parse(true)
v.date.parse(new Date())
v.symbol.parse(Symbol('x'))
v.undefined.parse(undefined)
v.null.parse(null)
// catch-all types allows any value
v.any.parse('whatever')
v.unknown.parse('anything')
// never type allows no values
v.never.parse('will throw!') // throws
Coercion for primitives
Dilav provides convenient ways to coerce primitive values:
const schema = v.string.coerce
schema.parse('fred') // => "fred"
schema.parse(13) // => "13"
schema.parse(false) // => "false"
schema.email().min(5).parse('notEmail') // throws
These primitive schemas support coercion:
v.string.coerce.parse(true) // String(input)
v.number.coerce.parse(true) // Number(input)
v.boolean.coerce.parse('1') // Boolean(input)
v.bigInt.coerce.parse('1') // BigInt(input)
v.date.coerce.parse('1') // new Date(input)
Literals
Literal schemas represent a literal type and only parse that exact type.
v.literal('hello').parse('hello')
v.literal(7).parse(7)
v.literal(3n).parse(3n)
const aObject = { a: 1 }
v.literal(aObject).parse(aObject)
const aSymbol = Symbol('a')
v.literal(aSymbol).parse(aSymbol)
const aDate = new Date()
v.literal(aDate).parse(aDate)
// retrieve literal value
v.literal('hello').definition.literal // => 'hello'
Special Literal Types
NaN
v.NaN.parse(NaN)
v.customize.NaN({ invalidValueFn: (value) => `${value} is not NaN` }).parse(1) // throws
Null
v.null.parse(null)
v.customize.null({ invalidValueFn: (value) => `${value} is not null` }).parse(1) // throws
Nullish
v.nullish.parse(null)
v.nullish.parse(undefined)
// NOTE the .nullishL not .nullish
v.customize.nullishL({ invalidValueFn: (value) => `${value} is not nullish` }).parse(1) // throws
Any
v.any.parse('hello')
Unknown
v.unknown.parse('hello')
Never
v.never.parse(1) // throws
v.customize.never({ invalidValueFn: (value) => `${value} is not never` }).parse(1) // throws
Void
v.void.parse()
v.void.parse(undefined)
v.customize.void({ invalidValueFn: (value) => `${value} is not void` }).parse(1) // throws
Undefined
v.undefined.parse(undefined)
v.customize.undefined({ invalidValueFn: (value) => `${value} is not undefined` }).parse(1) // throws
True
v.true.parse(true)
v.customize.true((value) => `${value} is not true`).parse(false) // throws
False
v.false.parse(false)
v.customize.false((value) => `${value} is not false`).parse(true) // throws
Strings
v.string.parse('abc')
v.string.email().parse('[email protected]') // => '[email protected]'
v.string.coerce.parse(1) // => '1'
// a function can be provided that returns a custom error message:
const foo = v.string.custom({ parseStringError: (value) => `didn't parse` }).safeParse(1)
const fooError = v.firstErrorFromResultError(foo) // => `didn't parse`
Dilav includes the following string-specific validations:
// validations
v.string.max(5).parse('12345')
v.string.min(5).parse('12345')
v.string.length(5).parse('12345')
v.string.email().parse('[email protected]')
v.string.url().parse('http://url.com')
v.string.emoji().parse('😀')
v.string.uuid().parse('123e4567-e89b-12d3-a456-426614174000')
v.string.cuid().parse('ch72gsb320000udocl363eofy')
v.string.cuid2().parse('tz4a98xxat96iws9zmbrgj3a')
v.string.ulid().parse('01ARZ3NDEKTSV4RRFFQ69G5FAV')
v.string.regex(/.*/).parse('ABC')
v.string.includes('A').parse('ABC')
v.string.startsWith('A').parse('ABC')
v.string.endsWith('C').parse('ABC')
v.string.datetime().parse('2020-01-01T00:00:00Z')
v.string.ip().parse('192.168.1.1')
v.string.ipv4().parse('192.168.1.1')
v.string.ipv6().parse('2001:0db8:0000:0000:0000:ff00:0042:8329')
v.string.notEmpty().parse('ABC')
// custom validations can be added:
v.string
.customValidation((stringValue) =>
stringValue === stringValue.toUpperCase() ? undefined : 'error',
)
.parse('ABC')
One can often provide customised validation error messages when adding a validation:
v.string.max(5, (value) => `${value} is too long!`).parse('12345')
String validations have the following call signatures:
type ValidationError = string
type StringValidationFn = (value: string) => ValidationError | undefined
max(length: number, errorFn?: DefaultErrorFn['maximumStringLength']): StringValidationFn
min(length: number, errorFn?: DefaultErrorFn['minimumStringLength']): StringValidationFn
length(length: number, errorFn?: DefaultErrorFn['stringLength']): StringValidationFn
notEmpty(errorFn?: DefaultErrorFn['notEmptyString']): StringValidationFn
beOneOf(items: string[], errorFn?: DefaultErrorFn['beOneOf']): StringValidationFn
regex(regex: RegExp, invalidFn?: DefaultErrorFn['maximumStringLength']): StringValidationFn
email(errorFn?: DefaultErrorFn['validEmail']): StringValidationFn
cuid(errorFn?: DefaultErrorFn['validCuid']): StringValidationFn
cuid2(errorFn?: DefaultErrorFn['validCuid2']): StringValidationFn
uuid(errorFn?: DefaultErrorFn['validUuid']): StringValidationFn
url(errorFn?: DefaultErrorFn['validURL']): StringValidationFn
ulid(errorFn?: DefaultErrorFn['validUlid']): StringValidationFn
emoji(errorFn?: DefaultErrorFn['validEmoji']): StringValidationFn
ipv4(errorFn?: DefaultErrorFn['validIpv4']): StringValidationFn
ipv6(errorFn?: DefaultErrorFn['validIpv6']): StringValidationFn
ip(invalidIpFn?: DefaultErrorFn['validIp']): StringValidationFn
datetime(options?: { precision?: number; offset?: boolean; validDateTimeFn?: DefaultErrorFn['validDateTime']}): StringValidationFn
includes(includedString: string, position?: number, errorFn?: DefaultErrorFn['includes']): StringValidationFn
startsWith(startString: string, errorFn?: DefaultErrorFn['startsWith']): StringValidationFn
endsWith(endString: string, errorFn?: DefaultErrorFn['endsWith']): StringValidationFn
DefaultErrorFn contains all validation error messages with are used, and which can be customised.
ISO datetimes
The v.string.datetime()
method defaults to UTC validation: no timezone offsets with arbitrary sub-second decimal precision.
const datetime = v.string.datetime()
datetime.parse('2020-01-01T00:00:00Z')
datetime.parse('2020-01-01T00:00:00.123Z')
datetime.parse('2020-01-01T00:00:00.123456Z')
expect(() => datetime.parse('2020-01-01T00:00:00+02:00')).toThrow()
Timezone offsets can be allowed by setting the offset
option to true
.
const datetime = v.string.datetime({ offset: true })
datetime.parse('2020-01-01T00:00:00+02:00')
datetime.parse('2020-01-01T00:00:00.123+02:00')
datetime.parse('2020-01-01T00:00:00.123+0200')
datetime.parse('2020-01-01T00:00:00.123+02')
datetime.parse('2020-01-01T00:00:00Z')
You can additionally constrain the allowable precision
. By default, arbitrary sub-second precision is supported.
const datetime = v.string.datetime({ precision: 3 })
datetime.parse('2020-01-01T00:00:00.123Z')
expect(() => datetime.parse('2020-01-01T00:00:00.123456Z')).toThrow()
expect(() => datetime.parse('2020-01-01T00:00:00Z')).toThrow()
v.customize.string
v.customize.string(
options?: {
parseStringError? : (value: unknown) => string, // function that returns a string on parsing error
})
Numbers
Includes a handful of number-specific validations:
v.number.gt(5).parse(6)
v.number.gte(5).parse(5) // alias .min(5)
v.number.lt(5).parse(4)
v.number.lte(5).parse(5) // alias .max(5)
v.number.int().parse(5) // value must be an integer
v.number.positive().parse(1) // > 0
v.number.nonnegative().parse(0) // >= 0
v.number.negative().parse(-1) // < 0
v.number.nonpositive().parse(0) // <= 0
v.number.multipleOf(5).parse(25) // Evenly divisible by 5. Alias .step(5)
v.number.finite().parse(1) // value must be finite, not Infinity or -Infinity
v.number.safe() // value must be between Number.MIN_SAFE_INTEGER and Number.MAX_SAFE_INTEGER
Number validations have the following call signatures:
type ValidationError = string
type NumberValidationFn = (value: number) => ValidationError | undefined
greaterThan(number: number, errorFn?: DefaultErrorFn['greaterThan']): NumberValidationFn;
greaterThanOrEqualTo(number: number, errorFn?: DefaultErrorFn['greaterThanOrEqualTo']): NumberValidationFn;
lesserThan(number: number, errorFn?: DefaultErrorFn['lesserThan']): NumberValidationFn;
lesserThanOrEqualTo(number: number, errorFn?: DefaultErrorFn['lesserThanOrEqualTo']): NumberValidationFn;
integer(errorFn?: DefaultErrorFn['integer']): NumberValidationFn;
positive(errorFn?: DefaultErrorFn['positive']): NumberValidationFn;
nonNegative(errorFn?: DefaultErrorFn['nonNegative']): NumberValidationFn;
negative(errorFn?: DefaultErrorFn['negative']): NumberValidationFn;
nonPositive(errorFn?: DefaultErrorFn['nonPositive']): NumberValidationFn;
notNaN(errorFn?: DefaultErrorFn['notNaN']): NumberValidationFn;
multipleOf(number: number, errorFn?: DefaultErrorFn['multipleOf']): NumberValidationFn;
finite(errorFn?: DefaultErrorFn['finite']): NumberValidationFn;
safe(errorFn?: DefaultErrorFn['safe']): NumberValidationFn;
DefaultErrorFn contains all validation error messages with are used, and which can be customised.
v.customize.number
v.customize.number(
options?: {
parseNumberError? : (value: unknown) => string, // function that returns string on parsing error
}) // => v.Number
BigInts
Dilav includes a handful of bigint-specific validations.
v.bigInt.gt(5n)
v.bigInt.gte(5n) // alias `.min(5n)`
v.bigInt.lt(5n)
v.bigInt.lte(5n) // alias `.max(5n)`
v.bigInt.positive() // > 0n
v.bigInt.nonNegative() // >= 0n
v.bigInt.negative() // < 0n
v.bigInt.nonPositive() // <= 0n
type ValidationError = string
type BigIntValidationFn = (value: bigint) => ValidationError | undefined
greaterThan(bigint: bigint, errorFn?: DefaultErrorFn['bigIntGreaterThan']): BigIntValidationFn
greaterThanOrEqualTo(bigint: bigint,errorFn?: DefaultErrorFn['bigIntGreaterThanOrEqualTo']): BigIntValidationFn
lesserThan(bigint: bigint, errorFn?: DefaultErrorFn['bigIntLesserThan']): BigIntValidationFn
lesserThanOrEqualTo(bigint: bigint,errorFn?: DefaultErrorFn['bigIntLesserThanOrEqualTo']): BigIntValidationFn
integer(errorFn?: DefaultErrorFn['bigIntInteger']): BigIntValidationFn
positive(errorFn?: DefaultErrorFn['bigIntPositive']): BigIntValidationFn
nonNegative(errorFn?: DefaultErrorFn['bigIntNonNegative']): BigIntValidationFn
negative(errorFn?: DefaultErrorFn['bigIntNegative']): BigIntValidationFn
nonPositive(errorFn?: DefaultErrorFn['bigIntNonPositive']): BigIntValidationFn
DefaultErrorFn contains all validation error messages with are used, and which can be customised.
v.customize.bigInt
v.customize.bigInt(
options?: {
parseBigIntError? : (value: unknown) => string, // function that returns string on parsing error
}) // => v.BigInt
Booleans
v.boolean.parse(true)
v.boolean.beTrue().parse(true)
v.boolean.beFalse().parse(false)
v.customize.boolean
v.customize.bigInt(
options: {
parseBigIntError? : (value: unknown) => string, // function that returns string on parsing error
}) // => v.BigInt
Dates
v.date
validates Date
instances.
v.date.parse(new Date())
v.date.parse('2022-01-12T00:00:00.000Z') // throws
The following date-specific validations are provided:
v.date.min(new Date('1900-01-01'), (value) => `${value} too old`)
v.date.max(new Date(), (value) => `${value} too young`)
Coercion to Date
v.coerce.date
passes the input through new Date(input)
.
const dateSchema = v.date.coerce
dateSchema.parse('2023-01-11T00:00:01.000Z')
dateSchema.parse('2023-01-11')
dateSchema.parse('1/11/23')
dateSchema.parse(new Date('1/11/23'))
/* invalid dates */
dateSchema.parse('2023-13-11') // throws
dateSchema.parse('0000-00-00') // throws
v.customize.date
v.customize.date(
options?: {
parseDateError? : (value: unknown) => string, // function that returns string on parsing error
}) // => v.Date
Enums
Dilav supports three types of enums: string
enums, Typescript enums and const
enums.
string
enums
const animalSchema1 = v.enum(['Dog', 'Cat', 'Fish'])
// similar to, except enum, includes an enum property :
const animalSchema2 = v.union.literals(['Dog', 'Cat', 'Fish'])
type AnimalTypes = v.Infer<typeof animalSchema1> // "Dog" | "Cat" | "Fish"
animalSchema1.parse('Dog') // => 'Dog'
animalSchema1.parse('doggo') // => throws
console.log(animalSchema1.definition.literals) // => ['Dog', 'Cat', 'Fish']
console.log(animalSchema1.enum) // => {Dog: 'Dog', Cat: 'Cat', Fish: 'Fish'}
console.log(animalSchema1.enum.Dog === 'Dog') // => true
Due to limitations of Typescript, Dilav enums can't correctly infer string arrays of type string[]
, and so the as const
modifier is required:
const animalTypes = ['Dog', 'Cat', 'Fish'] as const // as const is required
const animalEnumSchema = v.enum(animalTypes)
A second options parameter may be passed to v.enum
of the type:
{ parseLiteralUnion?: DefaultErrorFn['parseLiteralUnion'] }
Typescript enums
Typescript enums takes a typescript enum or any valid object as input, and on parsing if it finds a matching value (default) or key, it returns the matched value or key.
enum fooEnum {
Cat = 1,
Dog,
}
const fooEnumSchema = v.enum(fooEnum)
type FooEnumSchema = v.Infer<typeof fooEnumSchema> // fooEnum
fooEnumSchema.parse('Cat') // => 'Cat'
fooEnumSchema.parse(1) // => 1
fooEnumSchema.parse('Rat') // throws
console.log(fooEnumSchema.enum) // => the original fooEnum
A second options parameter may be passed to v.enum
of the type: { parseLiteralUnion?: DefaultErrorFn['parseLiteralUnion']; matchType?: 'key' | 'value' | 'either' }
The matchType
option specifies whether the value should be matched on property key, or property value, or both. The default is'value'
const
enums
It also works with const
objects:
const fooEnum = {
Cat: 1,
Dog: 'Dog',
} as const
const fooEnumSchema = v.enum(fooEnum)
type FooEnumSchema = v.Infer<typeof fooEnumSchema> // 1 | "Dog"
fooEnumSchema.parse(1)
fooEnumSchema.parse('Dog')
fooEnumSchema.parse('Cat') // throws
fooEnumSchema.parse('Rat') // throws
console.log(fooEnumSchema.enum) // => the original fooEnum
A second options parameter may be passed to v.enum
of the type: { parseLiteralUnion?: DefaultErrorFn['parseLiteralUnion']; matchType?: 'key' | 'value' | 'either' }
The matchType
option specifies whether the value should be matched on property key, or property value, or both. The default is'value'
Optionals
Schemas can be made optional with v.optional()
.
const schema = v.optional(v.string)
schema.parse(undefined) // => returns undefined
type Schema = v.Infer<typeof schema> // string | undefined
Equivalently one can also call the .optional()
method on an existing schema.
const user = v.object({
username: v.string.optional(),
})
type User = v.Infer<typeof user> // { username?: string | undefined };
The wrapped schema can be extracted via .definition.wrappedSchema
const stringSchema = v.string
const optionalString = stringSchema.optional()
optionalString.definition.wrappedSchema // => stringSchema
// alternatively one can extract it via:
const unwrappedSchema = optionalString.required()
Nullables
nullable
types can be created with v.nullable
:
const nullableString = v.nullable(v.string)
nullableString.parse('asdf') // => "asdf"
nullableString.parse(null) // => null
Or use the .nullable()
method.
const schema = v.string.nullable()
type Schema = v.Infer<typeof schema> // string | null
The wrapped schema can be extracted via .definition.wrappedSchema
const stringSchema = v.string
const nullableString = stringSchema.nullable()
nullableString.definition.wrappedSchema // => stringSchema
Nullishables
nullish
type (value|undefined|null)
can be created with v.nullishable
:
const nullishString = v.nullishable(v.string)
nullishString.parse('asdf') // => "asdf"
nullishString.parse(null) // => null
nullishString.parse(undefined) // => undefined
Or use the .nullish()
method.
const schema = v.string.nullish()
type Schema = v.Infer<typeof schema> // string | null | undefined
The wrapped schema can be extracted via .definition.wrappedSchema
const nullishString = v.string.nullish()
nullishString.definition.wrappedSchema // => v.string
Objects
Dilav parses the object provided and by default returns the original object. However if a transformation method is applied to any one of the properties, then a new object is returned with the transformed value(s), and copies of all other properties.
const foo = v.object({
name: v.string,
age: v.number,
})
type Foo = v.Infer<typeof foo> // { name: string; age: number; }
// The `unmatchedPropertySchema` is used to parse all unmatched properties of an object.
// if omitted the default is v.never - meaning no additional properties are permitted.
const bar = v.object({ name: v.string }, v.any)
type Bar = v.Infer<typeof bar> // { name: string } & { [P: keyof any]: any; }
.definition
Use .definition.propertySchemas
to access the schema for a particular key.
foo.definition.propertySchemas.name.parse('string') // => string schema
foo.definition.propertySchemas.age.parse(1) // => number schema
foo.definition.unmatchedPropertySchema.parse(1) // => never, so throws
.extends
Additional fields can be added to an object schema with the .extend
method.
const fooWithType = foo.extends({
type: v.string,
}) // { name: string; age: number; type: string; }
.extend
will overwrite any existing fields.
.merge
Merges schemas and if they share keys, the properties of merged schema overrides the initial schema. The last schemas' unmatchedPropertySchema
will be used.
const foo = v.object({ items: v.array(v.string) })
const idObject = v.object({ id: v.number })
const fooWithId1 = foo.merge(idObject).parse({ items: ['A'], id: 1 })
// similar to:
const fooWithId2 = foo
.extends(idObject.definition.propertySchemas, idObject.definition.unmatchedPropertySchema)
.parse({ items: ['A'], id: 1 })
whilst similar, .and
intersects each object and its properties, whereas merge overrides prior objects.
.pick/.omit
Allows one to pick or omit certain keys from an object. Note this changes the schema, and will not impact any objects parsed by the changed schema. This means parsed objects will have to match the changed schema.
const foo = v.object({
id: v.string,
name: v.string,
owners: v.array(v.string),
})
const nameAndIdOnly = foo.pick('name', 'id') // { name: string;id: string; }
const omitIdAndName = foo.omit('name', 'id') // { owners: string[]; }
.partial
Makes all properties optional.
const foo = v.object({ email: v.string, name: v.string })
const partialFoo = foo.partial() // { email?: string ; name?: string ; }
const partialName = foo.partial('name') // { email: string ; name?: string ; }
.deepPartial
.partial
marks items one level down optional
. and .deepPartial
does it for all included objects and arrays.
const foo = v.object({
name: v.string,
profile: v.object({ id: v.number }),
items: v.array(v.object({ value: v.string })),
})
const deepPartialFoo = foo.deepPartial().parse({})
/* {
name?: string | undefined;
profile?: { id?: number | undefined; } | undefined;
items?: { value?: string | undefined; }[] | undefined;
} */
.required
The .required
method unwraps all optional
properties. Only shallow.
const foo = v.object({
email: v.string.optional(),
name: v.string.optional(),
})
// { email?: string | undefined; name?: string | undefined }
const requiredFoo = foo.required()
// { email: string; name: string; }
.passThrough
By default object schemas set the unmatchedPropertySchema
to v.never
. .passThrough
sets it to v.unknown
. Equivalent to .catchAll(v.unknown)
const foo = v.object({ name: v.string })
expect(() =>
foo.parse({
name: 'fred',
extraProp: 12,
}),
).toThrow()
foo.passThrough().parse({
name: 'fred',
extraProp: 12,
}) // => { name: string; } & {[P: keyof Any]: unknown; }
.strict
Sets the unmatchedPropertySchema
to v.never
. Equivalent to .catchAll(v.never)
const foo = v.object({ name: v.string }, v.unknown)
foo.parse({
name: 'fred',
extraProp: 12,
})
expect(() =>
foo.strict().parse({
name: 'fred',
extraProp: 12,
}),
).toThrow()
.catchAll
Sets the unmatchedPropertySchema
to any valid schema.
const foo = v.object({ name: v.string }).catchAll(v.number)
foo.parse({
name: 'fred',
extraProp: 12,
}) // { name: string; } & { [P: keyof any]: number; }
v.object
v.object(
propertySchemas: { [key: keyof any]: v.MinimumSchema },
unmatchedPropertySchema?: v.MinimumSchema = v.Never,
options?: {
invalidObjectFn?: DefaultErrorFn['parseObject']
invalidObjectPropertiesFn?: DefaultErrorFn['invalidObjectPropertiesFn']
missingProperty?: DefaultErrorFn['missingProperty']
missingPropertyInDef?: DefaultErrorFn['missingPropertyInDef']
}) // => v.Object
Arrays
Dilav parses the array provided and by default returns the original unaltered array. However if a transformation method is applied to any one of array item schemas, then a new array is returned with the transformed element(s).
const array1 = v.array(v.string).parse([]) // string[]
// equivalent
const array2 = v.string.array().parse([]) // string[]
const array3 = v
.array([v.string, v.number, v.array(v.string).spread, v.object({ items: v.number })])
.parse(['a', 1, 'b', { items: 1 }]) // [string, number, ...string[], { items: number }]
.definition
.definition
accesses the schema for elements of the array.
const itemSchema = v.array(v.string).definition.itemSchema.parse('string')
const secondElementInArray = v.array([v.string, v.number]).definition.itemSchemas[1].parse(1)
validations
The following validations are supported:
v.array(v.number).max(3).parse([1, 2, 3])
v.array(v.number).min(3).parse([1, 2, 3])
v.array(v.number).length(3).parse([1, 2, 3])
v.array(v.number).nonEmpty().parse([1])
v.array(v.number)
.customValidation((arrayValue) => (arrayValue.includes(1) ? undefined : 'error'))
.parse([1])
// type call signatures:
type ValidationError = string
type ArrayValidationFn = (value: unknown[]) => ValidationError | undefined
minimumArrayLength(length: number, errorFn?: DefaultErrorFn['minimumArrayLength']): ArrayValidationFn
maximumArrayLength(length: number, errorFn?: DefaultErrorFn['maximumArrayLength']): ArrayValidationFn
requiredArrayLength(length: number,errorFn?: DefaultErrorFn['requiredArrayLength']): ArrayValidationFn
nonEmpty(errorFn?: DefaultErrorFn['arrayNonEmpty']): ArrayValidationFn
DefaultErrorFn contains all validation error messages with are used, and which can be customised.
.spread
.spread
within an array behaves similarly to the spread ...
in Typescript. A key limitation is that whilst multiple spreads are supported, only one can have an unknown length.
const foo = v
.array([v.string, v.number, v.array(v.string).spread, v.array([v.number, v.boolean]).spread])
.parse(['a', 1, 'b', 1, true]) // [string, number, ...string[], number, boolean]
v.array
type ArrayOptions = {
parseArray?: DefaultErrorFn['parseArray']
invalidArrayElementsFn?: DefaultErrorFn['invalidArrayElementsFn']
arrayDefinitionElementMustBeOptional?: DefaultErrorFn['arrayDefinitionElementMustBeOptional']
elementRequiredAt?: DefaultErrorFn['elementRequiredAt']
extraArrayItemsFn?: DefaultErrorFn['extraArrayItemsFn']
restCantFollowRest?: DefaultErrorFn['restCantFollowRest']
optionalElementCantFollowRest?: DefaultErrorFn['optionalElementCantFollowRest']
missingItemInItemSchemas?: DefaultErrorFn['missingItemInItemSchemas']
unableToSelectItemFromArray?: DefaultErrorFn['unableToSelectItemFromArray']
}
v.array(itemSchema: MinimumSchema, options?: ArrayOptions) // => v.ArrayInfinite
v.array(
itemSchemas: (MinimumSchema|MinimumArrayRestSchema)[],
options?: ArrayOptions
) // => v.ArrayFinite
Unions
Unions work similar to the |
in typescript. Each schema in the union tries to parse the value, and if one succeeds, the union parses, otherwise it will error.
const stringOrBoolSchema1 = v.union([v.string, v.boolean])
// identical to:
const stringOrBoolSchema2 = v.string.or(v.boolean) // => string | boolean
const stringOrBool = stringOrBoolSchema1.parse('foo') // => string | boolean
stringOrBoolSchema1.definition.schemas // = > [v.string, v.boolean]
Literal Unions
One could create a union of literals schemas and then parse against those, however Dilav includes a performance optimised way of parsing literals only unions.
const fooBarSchema1 = v.union.literals(['foo', 'bar'])
// similar too:
const fooBarSchema2 = v.enum(['foo', 'bar']).parse('foo') // => 'foo' | 'bar'
fooBarSchema1.parse('foo') // => 'foo' | 'bar'
fooBarSchema1.definition.literals // = > ['foo', 'bar']
Discriminated Unions - Key
Unions for objects can be computationally expensive as each property of the object must be parsed for conformance. In situations where each object has a unique discriminating key(s), the parser can first test for a match on only that key, and only if a match occurs will the rest of the properties be parsed for conformance.
const fooSchema = v.union.key('type', [
v.object({ type: v.literal('A'), data: v.string }),
v.object({ type: v.literal('B'), result: v.string }),
])
fooSchema.parse({ type: 'A', data: 'A TYPE' })
// => { type: "A"; data: string } | { type: "B"; result: string }
fooSchema.definition.schemas // => [ ... the schemas ]
fooSchema.definition.key // => 'type'
A second options object can be passed to the function:
{
keyNotFoundInDiscriminatedUnionDef?: DefaultErrorFn['keyNotFoundInDiscriminatedUnionDef']
// if true, then this tell dilav that the key is unique and so will parse only
// the first key matched schema. This will result in better performance if the key is unique.
// the default is false.
oneMatchOnly?: boolean
}
Discriminated Unions - Advanced
Advance discriminated unions provide more control over how unions are parsed. They parse against the match criteria, and if matched, they then parse the value against any provided schemas. This enables one to finely control what is parsed and can be used to eliminate any wasteful property parsing.
const fooSchema = v.union.advanced({ type: v.union.literals(['A', 'B']) }, [
v.union.advanced({ subType: v.union.literals(['S1', 'S2']) }, [
v.object({ type: v.literal('A'), subType: v.literal('S1') }),
v.object({ type: v.literal('B'), subType: v.literal('S2') }),
]),
])
fooSchema.parse({ type: 'A', subType: 'S1' })
fooSchema.parse({ type: 'B', subType: 'S2' })
expect(() => fooSchema.parse({ type: 'A', subType: 'S2' })).toThrow() // throws
// => { type: "A"; data: string } | { type: "B"; result: string }
fooSchema.definition.schemas // => [ ... the schemas ]
fooSchema.definition.matches // => the object used to match schemas
A third options object can be passed to the function:
{
noMatchFoundInUnion?: DefaultErrorFn['noMatchFoundInUnion']
}
Intersections
Intersections are similar to &
in Typescript. In general each item in the intersection must parse without error, for the intersection to successfully parse. By default, intersection errors on the first parsing error it encounters. If one requires a complete list of errors, one can set { breakOnFirstError: false }
as a second option to intersection.
const foo1 = v.intersection([v.enum(['A', 'B']), v.enum(['B', 'C'])])
// equivalent to:
const foo2 = v.enum(['A', 'B']).and(v.enum(['B', 'C']))
foo2.parse('B') // =>'B'
foo2.parse('A') // throws
When intersecting only object schemas a new object
schema is returned, with each property also intersected and with the unmatchedPropertySchema
set to an intersection of all the objects unmatchedPropertySchema
s
const foo1 = v
.intersection([v.object({ a: v.string }), v.object({ b: v.string })])
.parse({ a: 'A', b: 'B' }) // => { a: 'A', b: 'B' }
// equivalent to:
const foo2 = v
.object({ a: v.string })
.and(v.object({ b: v.string }))
.parse({ a: 'A', b: 'B' }) // => { a: 'A', b: 'B' }
When intersecting only infinite arrays the itemParser
's are intersected and a new array schema is returned
const foo1 = v
.intersection([v.array(v.object({ a: v.string })), v.array(v.object({ b: v.string }))])
.parse([{ a: 'A', b: 'B' }]) // => [{ a: 'A', b: 'B' }]
// equivalent to:
const foo2 = v
.array(v.object({ a: v.string }))
.and(v.array(v.object({ b: v.string })))
.parse([{ a: 'A', b: 'B' }]) // => [{ a: 'A', b: 'B' }]
Promises
Promises parses as follows: the promise being parsed is validated for conformance to being PromiseLike - i.e. having a .then
and .catch
method. If not parsing will fail. The parser returns a new Promise that wraps the parsed Promise and which acts as a proxy Promise. When .then
or .catch
is called those methods are called on the parsed Promise. When .then
returns a value, it is parsed and if successful it is returned, alternatively the proxy Promise will reject with the failed validation.
const fooPromiseSchema = v.promise(v.string)
const fooPromise1 = fooPromiseSchema.parse(Promise.resolve('foo')) // => ValidatedPromise<string>
const result = await fooPromise1 // foo
const fooPromise2 = fooPromiseSchema.parse(Promise.resolve(1))
try {
await fooPromise2
} catch (e) {
expect(e.errors[0]).toEqual('1 is not a string')
}
A second options parameter may be passed to v.promise
of the type:
{ parsePromise: DefaultErrorFn['parsePromise'] }
InstanceOfs
Validates than an object is an instanceOf
a particular class.
class Foo {
prop: string
}
const foo = v.instanceOf(Foo).parse(new Foo())
A second options parameter may be passed to v.instanceOf
of the type:
{ parseInstanceOf: DefaultErrorFn['parseInstanceOf'] }
Records
Record schemas are used to validate types such as { [k: string]: number }
.
// if only one schema is supplied, it's assumed that only the values of the object will be validated:
const foo1 = v.record(v.number).parse({ a: 1 }) // Record<string, number>
expect(() => v.record(v.number).parse({ a: '1' })).toThrow()
// if two schemas are supplied, the first will validate the property name, and the second its value:
const foo2 = v.record(v.string.min(5), v.number).parse({ abcde: 1 }) // Record<string, number>
expect(() => v.record(v.string.min(5), v.number).parse({ a: 1 })).toThrow()
As JavaScript casts all object keys to strings at this time only object keys of type string are supported. One could create a custom validator to parse numerical string property names.
A third options parameter may be passed to v.record
of the type:
{ parseRecord?: DefaultErrorFn['parseRecord']; breakOnFirstError?: boolean }
Maps
const foo = v.map([v.string, v.number]).parse(new Map([['apple', 1]])) // => Map<string, number>
expect(() => v.map([v.string, v.number]).parse(new Map([['apple', '1']]))).toThrow()
Map schemas can be further constrained with the following validation methods.
v.map([v.string, v.number])
.nonempty()
.parse(new Map([['apple', 1]])) // must contain at least one item
v.map([v.string, v.number])
.min(1)
.parse(new Map([['apple', 1]])) // must contain 1 or more items
v.map([v.string, v.number])
.max(1)
.parse(new Map([['apple', 1]])) // must contain 1 or fewer items
v.map([v.string, v.number])
.size(1)
.parse(new Map([['apple', 1]])) // must contain 1 items exactly
v.map([v.string, v.number])
.customValidation((valueMap) => (valueMap.size === 1 ? undefined : 'error'))
.parse(new Map([['apple', 1]]))
A second options parameter may be passed to v.map
of the type:
{ parseMap?: DefaultErrorFn['parseMap']; breakOnFirstError?: boolean }
Sets
const foo = v.set(v.number).parse(new Set([1])) // => Set<number>
expect(() => v.set(v.number).parse(new Set(['1']))).toThrow()
Set schemas can be further constrained with the following validation methods.
v.set(v.number)
.nonempty()
.parse(new Set([1])) // must contain at least one item
v.set(v.number)
.min(1)
.parse(new Set([1])) // must contain 1 or more items
v.set(v.number)
.max(1)
.parse(new Set([1])) // must contain 1 or fewer items
v.set(v.number)
.size(1)
.parse(new Set([1])) // must contain 1 items exactly
v.set(v.number)
.customValidation((valueSet) => (valueSet.size === 1 ? undefined : 'error'))
.parse(new Set([1]))
A second options parameter may be passed to v.set
of the type:
{ parseSet?: DefaultErrorFn['parseSet']; breakOnFirstError?: boolean }
Recursive types
Recursive schemas can be defined but their type can't be statically inferred and will have to be provided manually:
const foo = v.object({ name: v.string })
interface RecursiveSchema extends v.Infer<typeof foo> {
items: this[]
}
const recursiveSchema: v.Lazy<RecursiveSchema> = foo.extends({
items: v.lazy(() => recursiveSchema.array()),
})
const x1 = recursiveSchema.parse({
name: 'A',
items: [
{
name: 'B',
items: [
{
name: 'C',
items: [],
},
],
},
],
})
Despite supporting recursive schemas, passing cyclical data will cause an infinite loop.
Functions
Once can define function schemas that validate inputs and outputs of any function. Functions are defined as follows:
const foo1 = v.function()
type Foo1 = v.Infer<typeof foo1> // => (...args: never[]) => never
const bar1 = v.function({ args: [v.string], returns: v.void }) // => (args_0: string) => void
// equivalent
const bar2 = v.function({ parameters: v.array([v.string]), returns: v.void }) // => (args_0: string) => void
// equivalent
const bar3 = v.function().args(v.string).returns(v.void)
// equivalent
const bar4 = v
.function()
.parameters(v.array([v.string]))
.returns(v.void)
const validationFn = bar4.parse((arg) => (arg === 'undefined' ? undefined : arg))
validationFn('undefined')
expect(() => validationFn('a')).toThrow() // returns 'a' which fails validations
expect(() => validationFn(1 as unknown as string)).toThrow() // 1 is not a valid string
Parsing a function returns a function that by default wraps the original function within a validation function. One can control what is returned from parsing by specifying the returnedFunction
option. Options include:
'validated'
- input parameter types and the output type is validated in the returned function (default).'inputValidated'
- only input parameter types are validated in the returned function'outputValidated'
- only the output type is validated in the returned function'original'
- the original function is returned without any validations wrapped around it.
const foo1 = v.function({ args: [v.string], returns: v.void }, { returnedFunction: 'original' })
const fn = foo1.parse((a) => a)
fn(1 as any) // returns 1 and won't throw as fn is not wrapped in a validation function.
The options parameter may also include:
{ parseFunctionError?: DefaultErrorFn['parseFunction'] }
One can access the input and output schemas from a function schema via:
const foo = v.function({ args: [v.string], returns: v.boolean })
foo.definition.returns.parse(true) // return type
foo.definition.parameters.parse(['hello']) // parameter array type
Custom Schemas
Custom schemas can be created by providing a function of type (value:unknown)=>boolean
which returns true
if the value is of the supplied type, or false
if it is not.
const pxSchema = v.custom<`${number}px`>((value) => /^\d+px$/.test(value as string))
type PxSchema = v.Infer<typeof pxSchema> // `${number}px`
pxSchema.parse('50px') // => "50px"
expect(() => pxSchema.parse('50vw')).toThrow() // throws;
A second options parameter may be passed to v.custom
of the type: DefaultErrorFn['parseCustom']
to control the error message returned.
Schema Methods
Schemas contain the following shared methods:
.parse
.parse(data: unknown): T
The .parse
parses the data
and returns it if it is valid. Otherwise, an error is thrown.
const stringSchema = v.string
stringSchema.parse('result') // => returns "result"
stringSchema.parse(123) // throws error
.parseAsync
.parseAsync(data:Promise<unknown>): Promise<T>
const stringSchema = v.string
const result = await stringSchema.parseAsync(Promise.resolve('result')) // => returns "result"
.safeParse
.safeParse(data:unknown): ResultError<T>
.safeParse
parses the data and returns either a result or an error in the form of a ResultError<T>
- see below.
type ValidationErrors = {
input: unknown
errors: string[]
}
type ResultError<T> = [error: ValidationErrors, result?: undefined] | [error: undefined, result: T]
const foo = v.string.safeParse('hello') // => [undefined, 'hello']
const fooResult = v.resultFromResultError(foo) // => 'hello'
const bar = v.string.safeParse(12) // => [{ input: 12, errors: ["'12 is not a string'"] }, undefined]
const barError = v.errorFromResultError(bar) // => { input: 12, errors: ["'12 is not a string'"] }
const barErrorString = v.firstErrorFromResultError(bar) // => '12 is not a string'
The result is a discriminated union, of type [error: ValidationErrors, result?: undefined] | [error: undefined, result: T]
so errors can be handled conveniently:
const result = v.string.safeParse('world')
if (v.isError(result)) {
// handle error case
const errorResult = result[0] // => { input: unknown; errors: string[] }
// equivalent:
const errorResult2 = v.errorFromResultError(result) // => { input: unknown; errors: string[] }
const firstErrorString = v.firstErrorFromResultError(result) // => string
// equivalent:
const firstErrorString2 = v.firstError(errorResult) // => string
} else {
// handle success case
const stringResult = result[1] // => string
// equivalent:
const stringResult2 = v.resultFromResultError(result) // => string
}
.safeParseAsync
An asynchronous version of safeParse
.
await v.string.safeParseAsync(Promise.resolve('result')) // => returns [undefined, "result"]
.optional
Returns an optional version of a schema.
const optionalString = v.string.optional() // string | undefined
// equivalent to
v.optional(v.string)
.nullable
Returns a nullable version of a schema.
const nullableString = v.string.nullable() // string | null
// equivalent to
v.nullable(v.string)
.nullish
Returns a "nullish" version of a schema.
const nullishString = v.string.nullish() // string | null | undefined
// equivalent to
v.nullish(v.string)
.array
Returns an array schema for the given type:
const stringArray = v.string.array() // string[]
// equivalent to
v.array(v.string)
.promise
Wraps schema in a promise schema:
const stringPromise1 = v.string.promise() // Promise<string>
// equivalent to
v.promise(v.string)
.or
Creates a union of schemas:
const stringOrNumber = v.string.or(v.number) // string | number
// equivalent to
v.union([v.string, v.number])
.and
Creates intersection types.
const nameAndAge = z.object({ name: v.string }).and(v.object({ age: v.number })) // { name: string } & { age: number }
// equivalent to
v.intersection(v.object({ name: v.string }), v.object({ age: v.number }))
.pipe
.pipe()
- pipes the output from one schema into the input of another schema, making it possible to chain schemas together:
const foo = v.string.pipe(v.enum(['A', 'B']), v.enum(['A'])) // -> string -> 'A' | 'B' -> 'A'
foo.parse('A')
foo.parse('B') // throws
const schema = v.string.transform((str) => str.length).pipe(v.number.min(5))
schema.parse('12345') // 5
schema.parse('1235') // throws
.pipe()
can work around some common issues with coerce
:
const toDate = v.date.coerce
v.isResult(toDate.safeParse(null)) // => true
const toDateSchema = v.union([v.number, v.string, v.date]).pipe(toDate)
v.isResult(toDateSchema.safeParse(null)) // => false
const toBigInt = v.bigInt.coerce
expect(() => toBigInt.safeParse(null)).toThrow() // => throws
const toBigIntSchema = v.union([v.string, v.number, v.bigInt, v.boolean]).pipe(toBigInt)
v.isResult(toBigIntSchema.safeParse(null)) // => false
.type
A string of the schema type (warning: not properly tested yet!)
.baseType
The Dilav schema type: 'infinite array' | 'finite array' | 'bigint' | 'boolean' | 'date' | 'enum' | 'instanceof' | 'intersection' | 'literal' | 'map' | 'set' | 'string' | 'symbol' | 'union' | 'discriminated union' | 'string union' | 'optional' | 'nullable' | 'nullish' | 'function' | 'object' | 'number' | 'promise' | 'record' | 'lazy' | 'preprocess' | 'postprocess' | 'custom'
.customValidation
All schemas have a .customValidation
method, enabling one to supply any validation code that must be run as part of validation. A customValidatorFn
must return a string
which is the error message if validation fails, or undefined
if validation passes.
// customValidation<S extends unknown[]>( customValidatorFn: (value: Output, ...otherArgs: S)
// =>ValidationError | undefined, ...otherArgs: S): Schema
// example:
v.string.customValidation((value) => (value === 'A' ? 'cannot be A' : undefined))
.customValidationAsync
All schemas have a .customValidationAsync
method, enabling one to supply any validation code that must be run asynchronously as part of validation. A customValidatorFn
must return a promise which evaluates to either a string
which is the error message if validation fails, or undefined
if validation passes.
// customValidationAsync<S extends unknown[]>( customValidatorFn: (value: Output, ...otherArgs: S)
// => Promise<ValidationError | undefined>, ...otherArgs: S): Schema
// example:
v.string.customValidation(async (value) => ((await IsA(value)) ? 'cannot be A' : undefined))
transformation methods
By default Dilav returns the original unaltered parsed value. However sometimes one wants to transform that input into another value. Dilav provides the transformation methods below for that.
Note: Dilav doesn't allow transformed types as inputs into .and
, .or
, .union
, .intersection
, .partial
, .deepPartial
as this may produce unexpected and surprising results as explained below.
Note: transformation methods in a way break the separation of concerns: Dilav validates that a value matches a type, whereas transformation methods transform a type. I included them because Zod includes them, however I may change my mind.
// consider the illustrative example below. The intersection could transform `undefined` into `A`
// even though `A` may be invalid, it could transform it into `B`, a third alternative would be to return
// the original input value of `undefined` as valid.
const example = v
.intersection([v.enum(['A', 'B']).default('A'), v.enum(['B', 'C']).default('B')])
.parse(undefined)
// the above is resolved by the following explicit solutions:
const alt1 = v
.enum(['A', 'B'])
.default('A')
.pipe(v.intersection([v.enum(['A', 'B']), v.enum(['B', 'C'])]))
.parse(undefined) // => throws
const alt2 = v
.enum(['B', 'C'])
.default('B')
.pipe(v.intersection([v.enum(['A', 'B']), v.enum(['B', 'C'])]))
.parse(undefined) // => B
const alt3 = v
.union([v.intersection([v.enum(['A', 'B']), v.enum(['B', 'C'])]), v.undefined])
.parse(undefined) // => undefined
.preprocess
Processes data before parsing
// preprocess runs a function before parsing, the output of which is then parsed
v.number.preprocess((value) => (value as string).length).parse('hello') // => 5
.postprocess
Processes data after parsing and any validations. The input to the function is of type ResultError
and it must return a value of type ResultError
// postprocess runs a function after parsing, the output of which is then returned
v.string.postprocess(([error, value]) => [undefined, value?.toLowerCase()]).parse('HELLO') // => 'hello'
.transform
Processes data after parsing and any validations. It is similar to .postprocess
except the error case is handled automatically.
// transform runs a function after parsing, the output of which is then returned
v.string.transform((value) => value.toLowerCase()).parse('HELLO') // => 'hello'
.catch
Replaces an error value, with the value specified.
// if parsing returns an error, catch replaces the error with the `catchValue`
v.string.catch('default on error').parse(1) // => 'default on error'
// catch with objects may produce surprising results:
const foo = v.object({ inner: v.string.catch('does change object') }).catch({
inner: 'hello',
})
foo.parse(undefined)) // => { inner: 'hello' }
foo.parse({}) // => { inner: 'does change object' }
foo.parse({ inner: undefined }) // => { inner: 'does change object' }
.default
If the input value is undefined, a default value is substituted.
// if value being parsed is undefined, then it is replaced with the `defaultValue` before parsing
v.string.default('default on undefined').parse(undefined) // => 'default on undefined'
Other Topics
Type inference
You can extract the Typescript type of any schema with v.Infer<typeof mySchema>
.
const stringSchema = v.string
type StringSchema = v.Infer<typeof stringSchema> // string
const foo1: StringSchema = 13 // error
const foo2: StringSchema = 'hello' // no error
Writing generic functions
Dilav provides a number of types which one can use to write generics:
v.MinimumSchema
is the root type from which all other schemas inherit - so one can use it as a kind ofany
for schemas.v.MinimumArraySchema
- the root type from which all array schemas inheritv.MinimumObjectSchema
- the root type from which the object schema inheritsv.MinimumArrayRestSchema
- the root type forv.array(...).spread
All Dilav schemas have one of these types associated with them:
v.Boolean
v.BigInt
v.Date
v.Enum
v.InstanceOf
v.Map
v.Set
v.Number
v.Lazy
v.Record
v,String
v.Symbol
v.Function
v.Custom
v.Object
v.Preprocess
v.PostProcess
v.Default
v.Catch
v.Union
v.Optional
v.Nullable
v.Nullish
v.Promise
v.Intersection
v.Literal
v.VNaN
- the literal NaNv.Undefined
- the literal undefinedv.Null
- the literal nullv.NullishL
- the literal null|undefinedv.Any
- the literal anyv.Unknown
- the literal unknownv.Never
- the literal neverv.Void
- the literal void
function validate<T extends v.MinimumSchema>(schema: T) {
return (x) => schema.safeParse(x)
}
function foo<T extends v.String>(stringSchema: T) {
return stringSchema.optional()
}
foo(v.string.max(1)).parse('a') // => string | undefined
Error handling
Dilav provides a range of methods to customise it's error handling:
// a custom error function(s) can be provided to schemas that have a .custom method:
const foo = v.string.custom({ parseStringError: () => `not a string!` }).safeParse(1)
if (v.isError(foo)) console.log(foo[0].errors[0]) // => `not a string!`
// a custom error function can also be provided for most validations:
const result2 = v.string.max(1, () => 'too long').safeParse('12')
if (v.isError(result2)) console.log(result2[0].errors[0]) // => `too long`
One can also set global error message functions:
// alternatively error messages can be set globally that will be the default
// for all schemas not supplied with a custom error function.
v.setGlobalErrorMessages({ parseString: () => `that's not a string!` })
const foo = v.string.safeParse(1)
if (v.isError(foo)) console.log(foo[0].errors[0]) // => `that's not a string!`
Dilav provides some helper utilities for working with results and errors:
v.isError
- returns true ifResultError
is an error, false otherwisev.isResult
- returns true ifResultError
is a result, false otherwisev.firstError
- returns the first error from aValidationErrors
object.v.firstErrorFromResultError
- returns the first error from aResultError
array. Throws if it's a result.v.resultFromResultError
- returns the results from aResultError
array. Throws if it's an error.v.errorFromResultError
- returns theValidationErrors
object from aResultError
array. Throws if it's a result.
const foo = v.string.custom({ parseStringError: () => `not a string!` }).safeParse(1)
if (v.isError(foo)) {
console.log(v.firstErrorFromResultError(foo)) // => `not a string!`
console.log(v.firstError(foo[0])) // => `not a string!`
}
Dilav provides some useful types for working with results:
v.ResultError
-ResultError<E, R> = [error: E, result?: undefined] | [error: undefined, result: R]
v.ValidationErrors
-{ input: unknown; errors: string[] }
v.ValidationError
- the error type of thrown validation errors - inherits fromError
, and includes the following additional properties:{ input: unknown; errors: string[], errorObject: v.ValidationErrors, readonly firstError: string }
v.SingleArrayValidationError
- return type of a single array error:[index: number, errors: string[]]
v.SingleObjectValidationError
- return type of a single object validation error:[key: string | number | symbol, errors: string[]]
DefaultErrorFn
All of the global default messages than can be set are listed in the errorFns.ts
file.