How to Create a Type from a Zod Schema

A recipe on how to infer a TypeScript type from a Zod schema, keeping your types in sync with your validation.

When I use Zod to define my data schemas, like in my Astro content.config.ts, I often need a corresponding TypeScript type for use in my components. Manually defining the type is a pain because it means I have two sources of truth to keep updated.

The best way to handle this is to generate the TypeScript type directly from the Zod schema.

Define Your Zod Schema

First, I have my Zod schema defined. For this example, I’ll use the Sidebar schema from my content.config.ts, which is used for my project and cookbook pages.

TypeScript
import { z } from "astro:content";

const SidebarLink = z.object({
    type: z.literal("Link"),
    title: z.string(),
    href: z.string(),
    external: z.boolean().optional()
});

const SidebarGroup = z.object({
    type: z.literal("Group"),
    title: z.string(),
    open: z.boolean().optional(),
    items: z.array(SidebarLink)
});

const SidebarItem = z.union([SidebarGroup, SidebarLink]);

const Sidebar = z.object({
    title: z.string(),
    items: z.array(SidebarItem)
});

Infer the TypeScript Type

With the schema defined, I can use z.infer to create a TypeScript type. This is how I create a Sidebar type that I can use in my Astro components.

TypeScript
export type Sidebar = z.infer<typeof Sidebar>;

Use the Inferred Type

Now I have a Sidebar type that is always in sync with my Zod schema. I can import and use this type in any of my .astro or .ts files.

For example, in a layout component like src/layouts/MinimalLayout.astro, I can use it to type the props.

Astro
---
import type { Sidebar } from "../content.config.ts";

interface Props {
    sidebar: Sidebar;
}

const { sidebar } = Astro.props;
---

<!-- Now I can use `sidebar` with full type safety -->
<nav>
    <h2>{sidebar.title}</h2>
    ...
</nav>

This way, if I ever update the Sidebar schema in content.config.ts, TypeScript will immediately tell me if I need to make changes in my components. It’s a simple way to keep my validation and types connected.