CodeLog

Kneel Before Zod!

Kneel Before Zod!

TypeScript has changed the game for JavaScript developers by adding static type checking, but it doesn’t automatically handle data validation. Especially when dealing with external sources like APIs or user inputs. Lets break down the challenges of data validation in TypeScript, explores possible solutions, and takes a closer look at Zod, a powerful validation library.

Why Data Validation Matters in TypeScript

Data validation is all about making sure the data you receive is in the right format and contains the right information. This is especially important when handling external data, like API responses, user input or data from local storage. When you define types in TypeScript, they help during development, but they don’t actually enforce anything at runtime. So even if you expect an API to return a certain structure, TypeScript won’t stop it from giving you something completely different. You’ve probably experienced this issue tons of times with errors like:

VM228:1 Uncaught TypeError: Cannot read properties of undefined (reading ‘something’)

Compile-Time vs. Runtime-Time Gap

One of the biggest challenges in TypeScript data validation is the difference between what TypeScript checks at compile time and what actually happens at runtime. For example, when you fetch data from an API, TypeScript assumes it matches your type definitions, but in reality, there’s no guarantee. Same issue when reading from localStorage. Even when JSON.parse() succeeds, there’s no guarantee that the data has the shape you’re expecting. This gap means that without extra validation, your app could end up working with incorrect or unexpected data.

interface User {
  id: number;
  email: string;
}

const fetchUser = async (id: number): Promise<User> => {
  const response = await fetch(`/api/users/${id}`);
  const data = await response.json();
  return data; // But nothing ensures data actually matches User interface
}

const retrieveUser = async (): User | null => {
  try {
    const data = localStorage.get('user');
    const user = JSON.parse(data);
    return user; // But nothing ensures data actually matches User interface
  catch {
    return null;
  }
}

API interfaces are contracts, and usually this is not an issue, specially if you are also the maintainer of the API.

Solutions for TypeScript Data Validation

Type Guards and Assertion Functions

TypeScript’s built-in type guards provide a simple validation mechanism: Type Guards Docs

function isUser(data: unknown): data is User {
  return (
    data !== null &&
    typeof data === "object" &&
    "id" in data &&
    typeof data.id === "number" &&
    "username" in data &&
    typeof data.username === "string" &&
    "email" in data &&
    typeof data.email === "string"
  );
}

// Usage
const processUser = (data: unknown) => {
  if (isUser(data)) {
    // TypeScript knows data is User here
    console.log(data.username);
  } else {
    throw new Error("Invalid user data");
  }
};

This approach works but becomes unwieldy for complex objects, requiring manual implementation of validation logic.

Zod as a Solution for TypeScript Validation

Zod is a TypeScript-first schema validation library with static type inference. It allows defining schemas that validate data at runtime while automatically inferring TypeScript types. Zod Docs

import { z } from "zod";

const UserSchema = z.object({
  id: z.string(),
  email: z.string().email(),
});

// Extract the inferred type
type User = z.infer<typeof UserSchema>;
// { id: string; email: string }

The retrieve from local storage function would look like:

const retrieveUser = async (): User | null => {
  try {
    const data = localStorage.get('user');
    const user = JSON.parse(data);
    const validatedUser = UserSchema.parse(user);
    return validatedUser; // User matches the type
  catch {
    return null;
  }
}

Pros of Zod

TypeScript-First Design

Zod was built specifically for TypeScript, resulting in excellent type inference and integration with TypeScript’s type system. This enables catching type errors during development rather than at runtime.

Schema-to-Type Inference

The z.infer<typeof schema> pattern allows extracting TypeScript types directly from validation schemas, ensuring perfect alignment between validation and types.

Comprehensive Schema Options

Zod supports a wide range of validation options, from simple primitives to complex structures including objects, arrays, tuples, unions, and even functions.

Handy Zod utility: validateSchemaOrThrow

Here is a handy utility for validating schemas. It will attempt to validate the schema. When it succeeds it returns the validated data. It will re-throw the combined zod errors when data is invalid.

import { z, ZodRawShape } from "zod";

export function validateSchemaOrThrow<T extends ZodRawShape>(
  schema: z.ZodObject<T>,
  data: any
): ReturnType<z.ZodObject<T>["parse"]> {
  const parsed = schema.safeParse(data);

  if (!parsed.success) {
    const error = parsed.error.issues.map(issue => issue.message).join(", ");
    throw new Error(error);
  }

  return parsed.data;
}

This is how it would end up being used in a framework route for example:

export async function POST(request: NextRequest) {
  try {
    const req = await request.json();
    const credentials = validateSchemaOrThrow(LoginSchema, req);
    const authUser = await loginUserOrThrow(credentials);

    return NextResponse.json({ data: authUser }, { status: 200 });
  } catch (err: any) {
    const error = err?.message || "Unexpected login error";
    return NextResponse.json({ error, data: null }, { status: 409 });
  }
}

More info at

https://zod.dev/