Skip to main content
Test Double company logo
Services
Services Overview
Holistic software investment consulting
Software Delivery
Accelerate quality software development
Product Management
Launch modern product orgs
Legacy Modernization
Renovate legacy software systems
DevOps
Scale infrastructure smoothly
Upgrade Rails
Update Rails versions seamlessly
Technical Recruitment
Build tech & product teams
Technical Assessments
Uncover root causes & improvements
Case Studies
Solutions
Accelerate Quality Software
Software Delivery, DevOps, & Product Delivery
Maximize Software Investments
Product Performance, Product Scaling, & Technical Assessments
Future-Proof Innovative Software
Legacy Modernization, Product Transformation, Upgrade Rails, Technical Recruitment
About
About
What's a test double?
Approach
Meeting you where you are
Founder's Story
The origin of our mission
Culture
Culture & Careers
Double Agents decoded
Great Causes
Great code for great causes
EDI
Equity, diversity & inclusion
Insights
All Insights
Hot takes and tips for all things software
Leadership
Bold opinions and insights for tech leaders
Developer
Essential coding tutorials and tools
Product Manager
Practical advice for real-world challenges
Say Hello
Test Double logo
Menu
Services
BackGrid of dots icon
Services Overview
Holistic software investment consulting
Software Delivery
Accelerate quality software development
Product Management
Launch modern product orgs
Legacy Modernization
Renovate legacy software systems
Cycle icon
DevOps
Scale infrastructure smoothly
Upgrade Rails
Update Rails versions seamlessly
Technical Recruitment
Build tech & product teams
Technical Assessments
Uncover root causes & improvements
Case Studies
Solutions
Solutions
Accelerate Quality Software
Software Delivery, DevOps, & Product Delivery
Maximize Software Investments
Product Performance, Product Scaling, & Technical Assessments
Future-Proof Innovative Software
Legacy Modernization, Product Transformation, Upgrade Rails, Technical Recruitment
About
About
About
What's a test double?
Approach
Meeting you where you are
Founder's Story
The origin of our mission
Culture
Culture
Culture & Careers
Double Agents decoded
Great Causes
Great code for great causes
EDI
Equity, diversity & inclusion
Insights
Insights
All Insights
Hot takes and tips for all things software
Leadership
Bold opinions and insights for tech leaders
Developer
Essential coding tutorials and tools
Product Manager
Practical advice for real-world challenges
Say hello
Developers
Developers
Developers
Software tooling & tips

Get safer TypeScript code with Zod: a practical guide

Discover the power of Zod in validating TypeScript schemas, making your code more robust and error-free. Perfect for developers looking to improve their TypeScript skills.
Joseph Lozano
|
January 29, 2023
Thank you! Your submission has been received!
Oops! Something went wrong while submitting the form.

Everyone knows that TypeScript is type-safe, right? So what happens when I do something like this?

const json = `{"value": 2}`;
const data = JSON.parse(json) as { value: number };

data in this case is typed as {value: number}, correctly.

If I were to have code that did data.foo, I would get an appropriate type error: Property 'foo' does not exist on type '{ value: number; }'.ts(2339)

So, what happens if I change the type, without changing the actual underlying JSON?

const json = `{"value": 2}`;
const data = JSON.parse(json) as { foo: number };
data.foo;

No more type error! If were to run this, data.foo would evaluate to undefined.

But lets go a step further.

data.foo.toString(). toString() is a valid method on a number, and this is what TypeScript thinks there is (because that’s what we told it).

If you were compile this with tsc, the compilation would succeed … but look what happens when you run the compiled javascript file:

console.log(data.foo.toString());
                     ^

TypeError: Cannot read properties of undefined (reading 'toString')

A TypeError!

We need a better way to type values such as JSON, where at runtime they can be anything.

Enter Zod

Zod is “TypeScript-first schema validation with static type inference.”

Let’s see it in practice.

Instead of defining a type with the as keyword, we define a schema with Zod.

We then safeParse our data using the schema:

import { z } from "Zod";
const json = `{"value": 2}`;
const schema = z.object({
  value: z.number(),
});
const parsed = schema.safeParse(JSON.parse(json));

if (parsed.error) throw new Error("Some Error");

const data = parsed.data;
data.value.toString();

‍Now data is entirely type-safe. If the schema validation fails, parsed.error would true, and we would get the early return. parsed.data cannot be undefined, we can be sure that it is a number, and that the methods we run on it will not fail—at least due to a type error.

More complex cases

Zod is very powerful.

Here is a more advanced case: say you have an API which can either return {success: true, value: 42} or it can return {success: false, reason: "some reason"}.

If success is true, we want to access the value, if false, then maybe log the reason.

Here is how you would type this:

type MyUnionType =
  | {
      success: true;
      value: number;
    }
  | {
      success: false;
      reason: string;
    };

Besides this type definition, you now have to validate that any JSON you might receive actually conforms to this type.

Here is how you might do it in Zod:

import { z } from "Zod";

const schema = z.discriminatedUnion("success", [
  z.object({ success: z.literal(true), value: z.number() }),
  z.object({ success: z.literal(false), reason: z.string() }),
]);

type UnionTypeFromZod = z.infer<typeof schema>;

If you inspect UnionTypeFromZod, you’d see it exactly matches MyUnionType. But we just had to write the validation schema, and let Zod infer the type! You write the parser and get the type for free!

You’ll need to check out the documentation at Zod.dev to find out exactly how powerful this tool is, but here are a few of my favorites:

z.string().email(); // makes sure a string is formatted as an email
z.enum(["value_a", "value_b"]); // make sure it is one of the given types
z.number().step(0.01); // make sure a number isn't subdived past one hundredth
z.number().positive(); // makes sure a number is positive
z.string().optional(); // fields in Zod are mandatory by default.

You can also transform fields while validating them:

const emailToDomain = z
  .string()
  .email()
  .transform((val) => val.split("@")[1]);

emailToDomain.parse("joseph.lozano@testdouble.com"); // => testdouble.com

Zod is a very powerful tool, and a great way to get increased type safety while writing TypeScript.

A concrete example

Let’s see how it’s used to parse a real JSON response from the Deck of Cards API. First, lets define schemas for the cards:

// Our cardValues and cardSuits are defined as consts, which we can then
// extract the `CardValueCode` and `CardSuitCode` types from using
// `keyof typeof`.
const cardValues = {
  "2": "2",
  "3": "3",
  "4": "4",
  "5": "5",
  "6": "6",
  "7": "7",
  "8": "8",
  "9": "9",
  "0": "10",
  J: "JACK",
  Q: "QUEEN",
  K: "KING",
  A: "ACE",
} as const;
type CardValueCode = keyof typeof cardValues;

const cardSuits = {
  C: "CLUBS",
  S: "SPADES",
  H: "HEARTS",
  D: "DIAMONDS",
} as const;
// Becomes the union type: `"C" | "S" | "H" | "D"`
type CardSuitCode = keyof typeof cardSuits;

// `codeSchema` is a bit complex so define it separately
const codeSchema = z
  .string()
  .length(2)
  // Use `refine` because it has custom logic for validation
  .refine((val) => {
    const [cardValue, suitCode] = val.split("");
    return (
      // Pass `cardValue as CardValueCode` because `Object.keys(cardValues)` is
      // strongly typed as an array with `CardValueCode`. Likewise for the suits.

      Object.keys(cardValues).includes(cardValue as CardValueCode) &&
      Object.keys(cardSuits).includes(suitCode as CardSuitCode)
    );
  });

const cardSchema = z.object({
  code: codeSchema,
  // Like refine, the `.url()` helper doesn't add any additional type
  // information, but is useful for runtime validation.
  image: z.string().url(),
  images: z.object({
    svg: z.string().url().endsWith(".svg"),
    png: z.string().url().endsWith(".png"),
  }),
  value: z.nativeEnum(cardValues),
  suit: z.nativeEnum(cardSuits),
});

So, Zod has not only validated our JSON response, but has done it in a composable manner, while automatically extracting type information. In fact, the only types we had to write were CardValueCode and CardSuitCode. Zod was able to infer everything else for us.

For these reasons, Zod is quickly gaining popularity, and already has integrations with many tools, such as react-hook-form for validating forms, and tRPC, for type-safe data fetching. If you have any unknown data coming into your system, Zod is the best way to validate it, and get type information while writing only one schema.

Related Insights

🔗
How to synchronize JSON date formats for accurate data comparison
🔗
A quick guide to TypeScript conditional types
🔗
A lesson in type safety with TypeScript, Remix and Prisma

Explore our insights

See all insights
Developers
Developers
Developers
You’re holding it wrong! The double loop model for agentic coding

Joé Dupuis has noticed an influx of videos and blog posts about the "correct" way of working with AI agents. Joé thinks most of it is bad advice, and has a better approach he wants to show you.

by
Joé Dupuis
Leadership
Leadership
Leadership
Don't play it safe: Improve your continuous discovery process to reduce risk

We often front-load discovery to feel confident before building—but that’s not real agility. This post explores how continuous learning reduces risk better than perfect plans ever could.

by
Doc Norton
Leadership
Leadership
Leadership
How an early-stage startup engineering team improved the bottom line fast

A fast-growing startup was burning cash faster than it could scale. Here’s how smart engineering decisions helped them improve the bottom line.

by
Jonathon Baugh
Letter art spelling out NEAT

Join the conversation

Technology is a means to an end: answers to very human questions. That’s why we created a community for developers and product managers.

Explore the community
Test Double Executive Leadership Team

Learn about our team

Like what we have to say about building great software and great teams?

Get to know us
Test Double company logo
Improving the way the world builds software.
What we do
Services OverviewSoftware DeliveryProduct ManagementLegacy ModernizationDevOpsUpgrade RailsTechnical RecruitmentTechnical Assessments
Who WE ARE
About UsCulture & CareersGreat CausesEDIOur TeamContact UsNews & AwardsN.E.A.T.
Resources
Case StudiesAll InsightsLeadership InsightsDeveloper InsightsProduct InsightsPairing & Office Hours
NEWSLETTER
Sign up hear about our latest innovations.
Your email has been added!
Oops! Something went wrong while submitting the form.
Standard Ruby badge
614.349.4279hello@testdouble.com
Privacy Policy
© 2020 Test Double. All Rights Reserved.