if you're happy and you know it, star this repo ⭐
Most projects will be able to upgrade from v1 to v2 without any breaking changes. Zod 2 is recommended for all new projects. Find a breakdown of the new features and a simple migration guide here: https://github.com/vriad/zod/tree/v2
npm install zod@beta
yarn add zod@beta
Zod is a TypeScript-first schema declaration and validation library. I'm using the term "schema" to broadly refer to any data type/structure, from a simple string
to a complex nested object.
Zod is designed to be as developer-friendly as possible. My goal is to eliminate duplicative type declarations wherever possible. With Zod, you declare a validator once and Zod will automatically infer the static TypeScript type. It's easy to compose simpler types into complex data structures.
Some other great aspects:
- Zero dependencies
- Plain JavaScript: works in browsers and Node.js
- Tiny: 8kb minified + zipped
- Immutability: methods (i.e.
.optional()
return a new instance - Concise, chainable interface
- Functional approach: parse, don't validate
I work on Zod in my free time, so if you're making money from a product that is built with Zod, I'd massively appreciate sponsorship at any level. For solo devs, I recommend the Chipotle Bowl tier or the Cup of Coffee tier. If you're making money from a product you built using Zod, consider the [Startup tier](Cup of Coffee tier). You can learn more about the tiers at github.com/sponsors/vriad.
Kevin Simper @kevinsimper |
Brandon Bayer @flybayer, creator of Blitz.js |
Bamboo Creative https://bamboocreative.nz |
To get your name + Twitter + website here, sponsor Zod at the Freelancer or Consultancy tier.
- Installation
- Primitives
- Literals
- Parsing
- Type inference
- Custom validation
- Strings
- Numbers
- Objects
- Records
- Arrays
- Unions
- Enums
- Intersections
- Tuples
- Recursive types
- Promises
- Instanceof
- Function schemas
- Errors
- Comparison
- Changelog
To install the latest version:
npm install --save zod
yarn add zod
-
Zod 1.x requires TypeScript 3.3+
Support for TS 3.2 was dropped with the release of zod@1.10 on 19 July 2020
-
You must enable
strictNullChecks
or usestrict
mode which includesstrictNullChecks
. Otherwise Zod can't correctly infer the types of your schemas!// tsconfig.json { // ... "compilerOptions": { // ... "strictNullChecks": true } }
Zod is a validation library designed for optimal developer experience. It's a TypeScript-first schema declaration library with rigorous inferred types, incredible developer experience, and a few killer features missing from the existing libraries.
- Zero dependencies (5kb compressed)
- Immutability; methods (i.e.
.optional()
return a new instance - Concise, chainable interface
- Functional approach ("Parse, don't validate!")
You can create a Zod schema for any TypeScript primitive.
import * as z from 'zod';
// primitive values
z.string();
z.number();
z.bigint();
z.boolean();
z.date();
// empty types
z.undefined();
z.null();
z.void();
// catch-all types
z.any();
z.unknown();
const tuna = z.literal('tuna');
const twelve = z.literal(12);
const tru = z.literal(true);
Currently there is no support for Date or bigint literals in Zod. If you have a use case for this feature, please file an issue.
.parse(data:unknown): T
Given any Zod schema, you can call its .parse
method to check data
is valid. If it is, a value is returned with full type information! Otherwise, an error is thrown.
IMPORTANT: After Zod 1.11, the value returned by
.parse
is a deep clone of the variable you passed in. This was also the case in zod@1.4 and earlier. The only exception to this isUnion
andIntersection
schemas, which return the same value you pass in.
const stringSchema = z.string();
stringSchema.parse('fish'); // => returns "fish"
stringSchema.parse(12); // throws Error('Non-string type: number');
.safeParse(data:unknown): { success: true; data: T; } | { success: false; error: ZodError; }
If you don't want Zod to throw when validation errors occur, you can use .safeParse
. This method returns an object, even if validation errors occur:
stringSchema.safeParse(12);
// => { success: false; error: ZodError }
stringSchema.safeParse('billie');
// => { success: true; data: 'billie' }
Because the result is a discriminated union you can handle errors very conveniently:
const result = stringSchema.safeParse('billie');
if (!result.success) {
// handle error then return
return;
}
// underneath the if statement, TypeScript knows
// that validation passed
console.log(result.data);
Errors thrown from within refinement functions will not be caught.
.check(data:unknown)
You can also use a Zod schema as a type guard using the schema's .check()
method, like so:
const stringSchema = z.string();
const blob: any = 'Albuquerque';
if (stringSchema.check(blob)) {
// blob is now of type `string`
// within this if statement
}
You can use the same method to check for invalid data:
const stringSchema = z.string();
const process = (blob: any) => {
if (!stringSchema.check(blob)) {
throw new Error('Not a string');
}
// blob is now of type `string`
// underneath the if statement
};
.refine(validator: (data:T)=>any, params?: RefineParams)
Zod was designed to mirror TypeScript as closely as possible. But there are many so-called "refinement types" you may wish to check for that can't be represented in TypeScript's type system. For instance: checking that a number is an Int or that a string is a valid email address.
For example, you can define a custom validation check on any Zod schema with .refine
:
const myString = z.string().refine(val => val.length <= 255, {
message: "String can't be more than 255 characters",
});
As you can see, .refine
takes two arguments.
-
The first is the validation function. This function takes one input (of type
T
— the inferred type of the schema) and returnsany
. Any truthy value will pass validation. (Prior to zod@1.6.2 the validation function had to return a boolean.) -
The second argument is a params object. You can use this to customize certain error-handling behavior:
type RefineParams = { // override error message message?: string; // appended to error path path?: (string | number)[]; // params object you can use to customize message // in error map params?: object; };
These params let you define powerful custom behavior. Zod is commonly used for form validation. If you want to verify that "password" and "confirm" match, you can do so like this:
const passwordForm = z
.object({
password: z.string(),
confirm: z.string(),
})
.refine(data => data.password === data.confirm, {
message: "Passwords don't match",
path: ['confirm'], // set path of error
})
.parse({ password: 'asdf', confirm: 'qwer' });
Because you provided a path
parameter, the resulting error will be:
ZodError {
errors: [{
"code": "custom_error",
"path": [ "confirm" ],
"message": "Passwords don't match"
}]
}
Note that the path
is set to ["confirm"]
, so you can easily display this error underneath the "Confirm password" textbox.
Important note, the value passed to the path
option is concatenated to the actual error path. So if you took passwordForm
from above and nested it inside another object, you would still get the error path you expect.
const allForms = z.object({ passwordForm }).parse({
passwordForm: {
password: 'asdf',
confirm: 'qwer',
},
});
would result in
ZodError {
errors: [{
"code": "custom_error",
"path": [ "passwordForm", "confirm" ],
"message": "Passwords don't match"
}]
}
You can extract the TypeScript type of any schema with z.infer<typeof mySchema>
.
const A = z.string();
type A = z.infer<typeof A>; // string
const u: A = 12; // TypeError
const u: A = 'asdf'; // compiles
We'll include examples of inferred types throughout the rest of the documentation.
There are a handful of string-specific validations.
All of these validations allow you to optionally specify a custom error message.
z.string().min(5);
z.string().max(5);
z.string().length(5);
z.string().email();
z.string().url();
z.string().uuid();
z.string().regex(regex);
Check out validator.js for a bunch of other useful string validation functions.
Like .refine
, The final (optional) argument is an object that lets you provide a custom error in the message
field.
z.string().min(5, { message: 'Must be 5 or more characters long' });
z.string().max(5, { message: 'Must be 5 or fewer characters long' });
z.string().length(5, { message: 'Must be exactly 5 characters long' });
z.string().email({ message: 'Invalid email address.' });
z.string().url({ message: 'Invalid url' });
z.string().uuid({ message: 'Invalid UUID' });
To see the email and url regexes, check out this file. To use a more advanced method, use a custom refinement.
There are a handful of number-specific validations.
z.number().min(5);
z.number().max(5);
z.number().int(); // value must be an integer
z.number().positive(); // > 0
z.number().nonnegative(); // >= 0
z.number().negative(); // < 0
z.number().nonpositive(); // <= 0
You can optionally pass in a params object as the second argument to provide a custom error message.
z.number().max(5, { message: 'this👏is👏too👏big' });
// all properties are required by default
const dogSchema = z.object({
name: z.string(),
neutered: z.boolean(),
});
type Dog = z.infer<typeof dogSchema>;
/*
equivalent to:
type Dog = {
name:string;
neutered: boolean;
}
*/
const cujo = dogSchema.parse({
name: 'Cujo',
neutered: true,
}); // passes, returns Dog
const fido: Dog = {
name: 'Fido',
}; // TypeError: missing required property `neutered`
Use .shape
to access an object schema's property schemas.
const Location = z.object({
latitude: z.number(),
longitude: z.number(),
});
const Business = z.object({
location: Location,
});
Business.shape.location; // => Location schema
You can combine two object schemas with .merge
, like so:
const BaseTeacher = z.object({ subjects: z.array(z.string()) });
const HasID = z.object({ id: z.string() });
const Teacher = BaseTeacher.merge(HasId);
type Teacher = z.infer<typeof Teacher>; // => { subjects: string[], id: string }
You're able to fluently chain together many .merge
calls as well:
// chaining mixins
const Teacher = BaseTeacher.merge(HasId)
.merge(HasName)
.merge(HasAddress);
IMPORTANT: the schema returned by
.merge
is the intersection of the two schemas. The schema passed into.merge
does not "overwrite" properties of the original schema. To demonstrate:
const Obj1 = z.object({ field: z.string() });
const Obj2 = z.object({ field: z.number() });
const Merged = Obj1.merge(Obj2);
type Merged = z.infer<typeof merged>;
// => { field: never }
// because no type can simultaneously be both a string and a number
To "overwrite" existing keys, use .extend
(documented below).
You can add additional fields an object schema with the .extend
method.
Before zod@1.8 this method was called
.augment
. Theaugment
method is still available for backwards compatibility but it is deprecated and will be removed in a future release.
const Animal = z
.object({
species: z.string(),
})
.extend({
population: z.number(),
});
⚠️ You can use.extend
to overwrite fields! Be careful with this power!
// overwrites `species`
const ModifiedAnimal = Animal.extend({
species: z.array(z.string()),
});
// => { population: number, species: string[] }
Object masking is one of Zod's killer features. It lets you create slight variations of your object schemas easily and succinctly. Inspired by TypeScript's built-in Pick
and Omit
utility types, all Zod object schemas have .pick
and .omit
methods that return a "masked" version of the schema.
const Recipe = z.object({
id: z.string(),
name: z.string(),
ingredients: z.array(z.string()),
});
To only keep certain keys, use .pick
.
const JustTheName = Recipe.pick({ name: true });
type JustTheName = z.infer<typeof JustTheName>;
// => { name: string }
To remove certain keys, use .omit
.
const NoIDRecipe = Recipe.omit({ id: true });
type NoIDRecipe = z.infer<typeof NoIDRecipe>;
// => { name: string, ingredients: string[] }
This is useful for database logic, where endpoints often accept as input slightly modified versions of your database schemas. For instance, the input to a hypothetical createRecipe
endpoint would accept the NoIDRecipe
type, since the ID will be generated by your database automatically.
This is a vital feature for implementing typesafe backend logic, yet as far as I know, no other validation library (yup, Joi, io-ts, runtypes, class-validator, ow...) offers similar functionality as of this writing (April 2020). This is one of the must-have features that inspired the creation of Zod.
Zod provides a convenience method for automatically picking all primitive or non-primitive fields from an object schema.
const Post = z.object({
title: z.string()
});
const User = z.object({
id: z.number(),
name: z.string(),
posts: z.array(Post)
});
const UserFields = User.primitives();
typeof UserFields = z.infer<typeof UserFields>;
// => { id: number; name; string; }
const UserRelations = User.nonprimitives();
typeof UserFields = z.infer<typeof UserFields>;
// => { posts: Post[] }
These schemas are considering "primitive":
- string
- number
- boolean
- bigint
- date
- null/undefined
- enums
- any array of the above types
- any union of the above types
Inspired by the built-in TypeScript utility type Partial, all Zod object schemas have a .partial
method that makes all properties optional.
Starting from this object:
const user = z.object({
username: z.string(),
location: z.object({
latitude: z.number(),
longitude: z.number(),
}),
});
/*
{ username: string, location: { city: number, state: number } }
*/
We can create a partial version:
const partialUser = user.partial();
/*
{
username?: string | undefined,
location?: {
city: number;
state: number;
} | undefined
}
*/
// equivalent to:
const partialUser = z.object({
username: user.shape.username.optional(),
location: user.shape.location.optional(),
});
Or you can use .deepPartial
:
const deepPartialUser = user.deepPartial();
/*
{
username?: string | undefined,
location?: {
latitude?: number | undefined;
longitude?: number | undefined;
} | undefined
}
*/
Important limitation: deep partials only work as expected in hierarchies of object schemas. It also can't be used on recursive schemas currently, since creating a recursive schema requires casting to the generic
ZodSchema
type (which doesn't include all the methods of theZodObject
class). Currently an improved version of Zod is under development that will have better support for recursive schemas.
By default, Zod object schemas do not allow unknown keys!
const dogSchema = z.object({
name: z.string(),
neutered: z.boolean(),
});
dogSchema.parse({
name: 'Spot',
neutered: true,
color: 'brown',
}); // Error(`Unexpected keys in object: 'color'`)
This is an intentional decision to make Zod's behavior consistent with TypeScript. Consider this:
type Dog = z.infer<typeof dogSchema>;
const spot: Dog = {
name: 'Spot',
neutered: true,
color: 'brown',
};
// TypeError: Object literal may only specify known
// properties, and 'color' does not exist in type Dog
TypeScript doesn't allow unknown keys when assigning to an object type, so neither does Zod (by default). If you want to allow this, just call the .nonstrict()
method on any object schema:
const dogSchemaNonstrict = dogSchema.nonstrict();
dogSchemaNonstrict.parse({
name: 'Spot',
neutered: true,
color: 'brown',
}); // passes
This change is reflected in the inferred type as well:
type NonstrictDog = z.infer<typeof dogSchemaNonstrict>;
/*
{
name:string;
neutered: boolean;
[k:string]: any;
}
*/
Record schemas are used to validate types such as this:
type NumberCache = { [k: string]: number };
If you want to validate that all the values of an object match some schema, without caring about the keys, you should use a Record.
const User = z.object({
name: z.string(),
});
const UserStore = z.record(User);
type UserStore = z.infer<typeof UserStore>;
// => { [k: string]: User }
This is particularly useful for storing or caching items by ID.
const userStore: UserStore = {};
userStore['77d2586b-9e8e-4ecf-8b21-ea7e0530eadd'] = {
name: 'Carlotta',
}; // passes
userStore['77d2586b-9e8e-4ecf-8b21-ea7e0530eadd'] = {
whatever: 'Ice cream sundae',
}; // TypeError
And of course you can call .parse
just like any other Zod schema.
UserStore.parse({
user_1328741234: { name: 'James' },
}); // => passes
You may have expected z.record()
to accept two arguments, one for the keys and one for the values. After all, TypeScript's built-in Record type does: Record<KeyType, ValueType>
. Otherwise, how do you represent the TypeScript type Record<number, any>
in Zod?
As it turns out, TypeScript's behavior surrounding [k: number]
is a little unintuitive:
const testMap: { [k: number]: string } = {
1: 'one',
};
for (const key in testMap) {
console.log(`${key}: ${typeof key}`);
}
// prints: `1: string`
As you can see, JavaScript automatically casts all object keys to strings under the hood.
Since Zod is trying to bridge the gap between static and runtime types, it doesn't make sense to provide a way of creating a record schema with numerical keys, since there's no such thing as a numerical key in runtime JavaScript.
There are two ways to define array schemas:
First, you can create an array schema with the z.array()
function; it accepts another ZodSchema, which defines the type of each array element.
const stringArray = z.array(z.string());
// inferred type: string[]
Second, you can call the .array()
method on any Zod schema:
const stringArray = z.string().array();
// inferred type: string[]
You have to be careful with the .array()
method. It returns a new ZodArray
instance. This means you need to be careful about the order in which you call methods. These two schemas are very different:
z.string()
.undefined()
.array(); // (string | undefined)[]
z.string()
.array()
.undefined(); // string[] | undefined
const nonEmptyStrings = z
.string()
.array()
.nonempty();
// [string, ...string[]]
nonEmptyStrings.parse([]); // throws: "Array cannot be empty"
nonEmptyStrings.parse(['Ariana Grande']); // passes
// must contain 5 or more items
z.array(z.string()).min(5);
// must contain 5 or fewer items
z.array(z.string()).max(5);
// must contain exactly 5 items
z.array(z.string()).length(5);
Zod includes a built-in z.union
method for composing "OR" types.
const stringOrNumber = z.union([z.string(), z.number()]);
stringOrNumber.parse('foo'); // passes
stringOrNumber.parse(14); // passes
Unions are the basis for defining optional schemas. An "optional string" is just the union of string
and undefined
.
const A = z.union([z.string(), z.undefined()]);
A.parse(undefined); // => passes, returns undefined
type A = z.infer<typeof A>; // string | undefined
Zod provides a shorthand way to make any schema optional:
const B = z.string().optional(); // equivalent to A
const C = z.object({
username: z.string().optional(),
});
type C = z.infer<typeof C>; // { username?: string | undefined };
Similarly, you can create nullable types like so:
const D = z.union([z.string(), z.null()]);
Or you can use the shorthand .nullable()
:
const E = z.string().nullable(); // equivalent to D
type E = z.infer<typeof D>; // string | null
You can create unions of any two or more schemas.
/* Custom Union Types */
const F = z
.union([z.string(), z.number(), z.boolean()])
.optional()
.nullable();
F.parse('tuna'); // => tuna
F.parse(42); // => 42
F.parse(true); // => true
F.parse(undefined); // => undefined
F.parse(null); // => null
F.parse({}); // => throws Error!
type F = z.infer<typeof F>; // string | number | boolean | undefined | null;
There are two ways to define enums in Zod.
An enum is just a union of string literals, so you could define an enum like this:
const FishEnum = z.union([
z.literal('Salmon'),
z.literal('Tuna'),
z.literal('Trout'),
]);
FishEnum.parse('Salmon'); // => "Salmon"
FishEnum.parse('Flounder'); // => throws
For convenience Zod provides a built-in z.enum()
function. Here's is the equivalent code:
const FishEnum = z.enum(['Salmon', 'Tuna', 'Trout']);
type FishEnum = z.infer<typeof FishEnum>;
// 'Salmon' | 'Tuna' | 'Trout'
Important! You need to pass the literal array directly into z.enum(). Do not define it separately, than pass it in as a variable! This is required for proper type inference.
Autocompletion
To get autocompletion with a Zod enum, use the .enum
property of your schema:
FishEnum.enum.Salmon; // => autocompletes
FishEnum.enum;
/*
=> {
Salmon: "Salmon",
Tuna: "Tuna",
Trout: "Trout",
}
*/
You can also retrieve the list of options as a tuple with the .options
property:
FishEnum.options; // ["Salmon", "Tuna", "Trout"]);
⚠️ nativeEnum()
requires TypeScript 3.6 or higher!
Zod enums are the recommended approach to defining and validating enums. But there may be scenarios where you need to validate against an enum from a third-party library, or perhaps you don't want to rewrite your existing enums. For this you can use z.nativeEnum()
.
Numeric enums
enum Fruits {
Apple,
Banana,
}
const FruitEnum = z.nativeEnum(Fruits);
type FruitEnum = z.infer<typeof FruitEnum>; // Fruits
FruitEnum.parse(Fruits.Apple); // passes
FruitEnum.parse(Fruits.Banana); // passes
FruitEnum.parse(0); // passes
FruitEnum.parse(1); // passes
FruitEnum.parse(3); // fails
String enums
enum Fruits {
Apple = 'apple',
Banana = 'banana',
Cantaloupe, // you can mix numerical and string enums
}
const FruitEnum = z.nativeEnum(Fruits);
type FruitEnum = z.infer<typeof FruitEnum>; // Fruits
FruitEnum.parse(Fruits.Apple); // passes
FruitEnum.parse(Fruits.Cantaloupe); // passes
FruitEnum.parse('apple'); // passes
FruitEnum.parse('banana'); // passes
FruitEnum.parse(0); // passes
FruitEnum.parse('Cantaloupe'); // fails
Const enums
The .nativeEnum()
function works for as const
objects as well. as const
required TypeScript 3.4+!
const Fruits = {
Apple: 'apple',
Banana: 'banana',
Cantaloupe: 3,
} as const;
const FruitEnum = z.nativeEnum(Fruits);
type FruitEnum = z.infer<typeof FruitEnum>; // "apple" | "banana" | 3
FruitEnum.parse('apple'); // passes
FruitEnum.parse('banana'); // passes
FruitEnum.parse(3); // passes
FruitEnum.parse('Cantaloupe'); // fails
⚠️ Intersections are rarely useful. If you are trying to merge objects, use the.merge
method instead. It's more readable and will provide better error reporting.
Intersections are useful for creating "logical AND" types.
const a = z.union([z.number(), z.string()]);
const b = z.union([z.number(), z.boolean()]);
const c = z.intersection(a, b);
type c = z.infer<typeof C>; // => number
const stringAndNumber = z.intersection(z.string(), z.number());
type Never = z.infer<typeof stringAndNumber>; // => never
There is a dedicated guide on Zod's error handling system here: ERROR_HANDLING.md
There are a handful of other widely-used validation libraries, but all of them have certain design limitations that make for a non-ideal developer experience.
Doesn't support static type inference 😕 Immediate disqualification, sorry Joi!
https://github.com/jquense/yup
Yup is a full-featured library that was implemented first in vanilla JS, with TypeScript typings added later.
Differences
- Supports for casting and transformation
- All object fields are optional by default
- Non-standard
.required()
¹ - Missing object methods: (pick, omit, partial, deepPartial, merge, extend)
- Missing nonempty arrays with proper typing (
[T, ...T[]]
) - Missing promise schemas
- Missing function schemas
- Missing union & intersection schemas
¹ Yup has a strange interpretation of the .required()
is odd and non-standard. Instead of meaning "not undefined", Yup uses it to mean "not empty". So yup.string().required()
will not accept an empty string, and yup.array(yup.string()).required()
will not accept an empty array. For Zod arrays there is a dedicated .nonempty()
method to indicate this, or you can implement it with a custom validator.
https://github.com/gcanti/io-ts
io-ts is an excellent library by gcanti. The API of io-ts heavily inspired the design of Zod.
In our experience, io-ts prioritizes functional programming purity over developer experience in many cases. This is a valid and admirable design goal, but it makes io-ts particularly hard to integrate into an existing codebase with a more procedural or object-oriented bias. For instance, consider how to define an object with optional properties in io-ts:
import * as t from 'io-ts';
const A = t.type({
foo: t.string,
});
const B = t.partial({
bar: t.number,
});
const C = t.intersection([A, B]);
type C = t.TypeOf<typeof C>;
// returns { foo: string; bar?: number | undefined }
You must define the required and optional props in separate object validators, pass the optionals through t.partial
(which marks all properties as optional), then combine them with t.intersection
.
Consider the equivalent in Zod:
const C = z.object({
foo: z.string(),
bar: z.string().optional(),
});
type C = z.infer<typeof C>;
// returns { foo: string; bar?: number | undefined }
This more declarative API makes schema definitions vastly more concise.
io-ts
also requires the use of gcanti's functional programming library fp-ts
to parse results and handle errors. This is another fantastic resource for developers looking to keep their codebase strictly functional. But depending on fp-ts
necessarily comes with a lot of intellectual overhead; a developer has to be familiar with functional programming concepts and the fp-ts
nomenclature to use the library.
- Supports codecs with serialization & deserialization transforms
- Supports branded types
- Supports advanced functional programming, higher-kinded types,
fp-ts
compatibility - Missing object methods: (pick, omit, partial, deepPartial, merge, extend)
- Missing nonempty arrays with proper typing (
[T, ...T[]]
) - Missing lazy/recursive types
- Missing promise schemas
- Missing function schemas
- Missing union & intersection schemas
- Missing support for parsing cyclical data (maybe)
- Missing error customization
https://github.com/pelotom/runtypes
Good type inference support, but limited options for object type masking (no .pick
, .omit
, .extend
, etc.). No support for Record
s (their Record
is equivalent to Zod's object
). They DO support branded and readonly types, which Zod does not.
- Supports "pattern matching": computed properties that distribute over unions
- Supports readonly types
- Missing object methods: (pick, omit, partial, deepPartial, merge, extend)
- Missing nonempty arrays with proper typing (
[T, ...T[]]
) - Missing lazy/recursive types
- Missing promise schemas
- Missing union & intersection schemas
- Missing error customization
- Missing record schemas (their "record" is equivalent to Zod "object")
https://github.com/sindresorhus/ow
Ow is focused on function input validation. It's a library that makes it easy to express complicated assert statements, but it doesn't let you parse untyped data. They support a much wider variety of types; Zod has a nearly one-to-one mapping with TypeScript's type system, whereas ow lets you validate several highly-specific types out of the box (e.g. int32Array
, see full list in their README).
If you want to validate function inputs, use function schemas in Zod! It's a much simpler approach that lets you reuse a function type declaration without repeating yourself (namely, copy-pasting a bunch of ow assertions at the beginning of every function). Also Zod lets you validate your return types as well, so you can be sure there won't be any unexpected data passed downstream.
View the changelog at CHANGELOG.md