Use Zod with TypeScript

Zod is a TypeScript-first schema validation library. Learn how to define schemas, infer types, parse data safely, and handle validation errors so you never trust unvalidated data.

7 min read

Zod is a TypeScript-first validation library. You define a schema that describes the shape of your data, and Zod gives you two things: a runtime validator that checks whether unknown data matches the shape, and a TypeScript type inferred from that schema. You write the schema once and get both compile-time and runtime safety.

typescripttypescript
import { z } from "zod";
 
const UserSchema = z.object({
  id: z.number(),
  name: z.string(),
  email: z.email(),
});
 
type User = z.infer<typeof UserSchema>;

The User type is { id: number; name: string; email: string }, inferred directly from the schema definition.

If you change the schema, the type updates automatically. There is no second source of truth.

Parsing and Validating Data

Zod provides two methods for validating data: parse and safeParse.

The first returns the validated data on success and throws a detailed error on failure. Use it when invalid data should stop execution.

typescripttypescript
const raw = '{"id": 1, "name": "Alice", "email": "alice@example.com"}';
const user = UserSchema.parse(JSON.parse(raw));
 
console.log(user.name);

If any field fails validation, the call throws with a detailed error describing which field was wrong and why. This is far more informative than a generic boolean from a hand-written type guard.

The second method returns a result object instead of throwing. Use it when you want to handle validation failures gracefully.

typescripttypescript
const result = UserSchema.safeParse(JSON.parse(raw));
 
if (result.success) {
  console.log(result.data.name);
} else {
  console.error(result.error.issues);
}

The success property discriminates the result. On success, the data field contains the validated value. On failure, the error field lists each problem with its path, message, and code.

MethodReturns on successReturns on failureUse when
parseValidated valueThrows an errorInvalid data should crash
safeParseSuccess object with dataFailure object with errorFailure is recoverable

Common Schema Types

Zod has primitives for every TypeScript type. Here are the most commonly used ones.

typescripttypescript
z.string();          // any string
z.email();           // string matching email format
z.url();             // string matching URL format
z.string().min(3);   // string with minimum length
z.number();          // any number
z.number().int();    // integer only
z.number().positive(); // greater than zero
z.boolean();         // true or false
z.date();            // Date instance
z.enum(["a", "b"]);  // union of string literals
z.literal("hello");  // exact value
z.undefined();       // undefined
z.null();            // null

Combine primitives with z.object for structured data and z.array for lists, passing the built-in checks straight in as arguments instead of writing a separate validation function.

typescripttypescript
const PostSchema = z.object({
  title: z.string().min(1, "Title is required"),
  tags: z.array(z.string()),
  published: z.boolean(),
  viewCount: z.number().int().nonnegative(),
});

Optional fields use .optional(), and the inferred type will include undefined unless a default value covers the gap instead, as shown below.

typescripttypescript
const ConfigSchema = z.object({
  port: z.number().optional(),
  debug: z.boolean().default(false),
});

A default provides a fallback value when the field is missing. The inferred type does not include undefined for defaulted fields because the default fills in the gap.

Refinements: Custom Validation Rules

Built-in methods cover common cases, but business rules often need custom logic. The refine method adds a predicate that runs after the base type check.

typescripttypescript
const PasswordSchema = z.string().refine(
  (value) => value.length >= 8,
  { error: "Password must be at least 8 characters" }
);

The first argument is the predicate. The second is an options object with an error message for the output.

For object-level rules that depend on multiple fields, call refine on the object schema instead of on a single field.

typescripttypescript
const BookingSchema = z.object({
  checkIn: z.date(),
  checkOut: z.date(),
}).refine(
  (data) => data.checkOut > data.checkIn,
  { error: "Check-out must be after check-in" }
);

The predicate receives the fully parsed object, so you can compare fields. This is how you enforce cross-field constraints.

Transforming Data During Parsing

Sometimes the data coming in is not exactly what you want to store. The transform method modifies the value after validation runs.

typescripttypescript
const NumberFromString = z.string().transform((value) => parseInt(value, 10));
 
const result = NumberFromString.parse("42");
console.log(result);
console.log(typeof result);

The output is the number 42. The schema accepted a string and produced a number, so the inferred type for this schema is number, not string.

Transforms are also useful for normalising data.

typescripttypescript
const TrimmedString = z.string().transform((value) => value.trim());
 
const result = TrimmedString.parse("  hello  ");
console.log(result);

The output is "hello". The transformation runs after the string check passes, so the incoming value is already known to be a string.

Composing and Extending Schemas

Real applications rarely have isolated schemas. Zod lets you compose schemas with extend, pick, and omit.

typescripttypescript
const Timestamped = z.object({
  createdAt: z.date(),
  updatedAt: z.date(),
});
 
const UserWithTimestamps = UserSchema.extend(Timestamped.shape);

The combined schema has all fields from both schemas. The inferred type includes id, name, email, createdAt, and updatedAt.

Use pick and omit to create derived schemas from existing ones.

typescripttypescript
const CreateUserSchema = UserSchema.omit({ id: true });
 
const PublicUserSchema = UserSchema.pick({
  id: true,
  name: true,
});

The first derived schema has name and email but not id. The second has only id and name.

The inferred types update accordingly, so each derived schema exposes only the fields it kept.

Parsing at the Boundary

The most effective place to use Zod is at the boundary where data enters your application. Parse API responses, form inputs, environment variables, and query parameters in one place, then pass the typed result inward.

typescripttypescript
async function fetchUser(id: string): Promise<User> {
  const response = await fetch(`/api/users/${id}`);
 
  if (!response.ok) {
    throw new Error(`HTTP ${response.status}`);
  }
 
  return UserSchema.parse(await response.json());
}

After this function returns, every caller can trust the User type. The validation happened once at the boundary. For more on this pattern, see validate API data in TypeScript. For the foundational concepts behind why validation is necessary, see runtime validation in TypeScript.

Rune AI

Rune AI

Key Insights

  • Zod schemas define the expected shape and validate it at runtime.
  • Use z.infer to extract TypeScript types directly from schemas.
  • z.object defines object shapes with typed, required, and optional fields.
  • Use safeParse to get a result object instead of throwing on invalid data.
  • Add custom rules with refine, transform data with transform, and compose schemas with extend.
RunePowered by Rune AI

Frequently Asked Questions

What does Zod give me that hand-written type guards do not?

Zod gives you detailed error messages showing exactly which field failed and why, automatic TypeScript type inference from schemas, and built-in transformations, refinements, and coercions. For large schemas, Zod is dramatically less code than hand-written guards.

Should I use Zod.parse or Zod.safeParse?

Use parse when invalid data should throw, such as in CLI tools or serverless functions where there is no recovery path. Use safeParse in user-facing code where you want to show a friendly error message instead of crashing.

Can Zod validate data that does not perfectly match my TypeScript types?

Yes. Zod supports transforms (convert a string to a number during parsing), coercions (try to coerce before checking), and refinements (add custom validation logic like minimum length or regex patterns). You can shape the data on the way in.

Conclusion

Zod replaces handwritten validation with concise, readable schemas. You define the shape once and get both runtime validation and TypeScript type inference. Use parse for code that should throw on bad data and safeParse for code that should handle errors gracefully. Start with the built-in types, add refinements for business rules, and use transforms to reshape data during parsing.