Yükleniyor...
Yükleniyor...
On TubeOnAI we wired Zod into every API boundary, every form, and every Firebase response — and the next month our error tracker stopped recording shape-mismatch bugs entirely. Notes on the Zod patterns I now reach for by default.
Early on at TubeOnAI, our error tracker (Sentry) was reporting a steady trickle of "cannot read property of undefined" bugs that all traced back to API responses not matching the TypeScript types we expected. The types said one shape, the API returned another, and the bug only surfaced when a user clicked through to a screen that depended on the missing field. We rewrote the data layer to push every API response through a Zod schema. The shape-mismatch bugs in Sentry dropped to zero the following month.
TypeScript is erased at runtime. The moment data enters your app from an API, a form, a URL parameter, or localStorage, your types are a hope rather than a guarantee. Zod is the most popular library for closing that gap — schemas that validate at runtime and infer types at compile time, from a single source of truth.
The Zod workflow inverts the usual TypeScript flow. Instead of writing a type and hoping the data matches, you write a schema that validates the data and get the type for free.
import { z } from 'zod';
// Define the schema once
const UserSchema = z.object({
id: z.string().uuid(),
email: z.string().email(),
age: z.number().int().min(0).max(150),
role: z.enum(['admin', 'user', 'guest']),
createdAt: z.coerce.date(),
metadata: z.record(z.unknown()).optional(),
});
// Type inferred from schema — no duplication
type User = z.infer<typeof UserSchema>;
// Parse runtime data safely
const result = UserSchema.safeParse(untrustedData);
if (result.success) {
// result.data is typed as User
console.log(result.data.email);
} else {
// result.error is a ZodError with detailed paths
console.error(result.error.issues);
}Bilgi
z.infer<typeof Schema> is the magic. Your schema is a single source of truth for both runtime validation and compile-time types. Update the schema, types update automatically.
Zod belongs at every boundary where untrusted data enters your system. API response parsing, form submission handling, URL parameter decoding, environment variable validation, localStorage reads, WebSocket messages — anywhere the TypeScript compiler has no way to verify shape.
Wrap your fetch calls in a helper that parses the response through a Zod schema. Your business logic never sees unvalidated data.
async function fetchTyped<T extends z.ZodTypeAny>(
url: string,
schema: T,
init?: RequestInit,
): Promise<z.infer<T>> {
const res = await fetch(url, init);
if (!res.ok) throw new Error(`HTTP ${res.status}`);
const json = await res.json();
return schema.parse(json);
}
// Usage — fully type-safe
const PostsSchema = z.array(z.object({
id: z.string(),
title: z.string(),
publishedAt: z.coerce.date(),
}));
const posts = await fetchTyped('/api/posts', PostsSchema);
// posts is Array<{ id: string; title: string; publishedAt: Date }>Zod pairs naturally with React Hook Form via @hookform/resolvers. One schema drives field-level validation, error messages, and the final submitted type.
import { zodResolver } from '@hookform/resolvers/zod';
import { useForm } from 'react-hook-form';
const SignupSchema = z.object({
email: z.string().email('Invalid email'),
password: z.string().min(8, 'At least 8 characters'),
confirm: z.string(),
}).refine((data) => data.password === data.confirm, {
message: 'Passwords do not match',
path: ['confirm'],
});
type SignupData = z.infer<typeof SignupSchema>;
function SignupForm() {
const form = useForm<SignupData>({
resolver: zodResolver(SignupSchema),
});
return <form onSubmit={form.handleSubmit(onSubmit)}>...</form>;
}Zod can transform data as it validates. Coerce strings to numbers, trim whitespace, uppercase emails, parse JSON — transformation and validation happen in one pass.
const EnvSchema = z.object({
PORT: z.coerce.number().int().positive(),
DATABASE_URL: z.string().url(),
DEBUG: z.coerce.boolean().default(false),
ALLOWED_ORIGINS: z.string().transform((s) => s.split(',').map((o) => o.trim())),
});
// Parse at startup — app crashes with clear error if misconfigured
export const env = EnvSchema.parse(process.env);İpucu
Use safeParse when you can recover from invalid data (form fields, optional features). Use parse when invalid data is unrecoverable (env vars at startup, critical API contracts). The difference is crashing early vs rendering an error message.
Zod is 15KB minified + gzipped. For most apps that is fine. If bundle size matters more than features, consider Valibot (3KB) or the newer ArkType which compiles schemas to validator functions. The API shape is similar enough that migration is mechanical.
On TubeOnAI, the Zod migration paid for itself by the second sprint — Sentry stopped flagging shape-mismatch errors, the form validation got tighter, and the engineers added schemas faster than I expected once the pattern was visible. Runtime validation is not optional for production TypeScript code. The only real choice is whether you validate with a schema library or by hand. Zod wins because it is expressive, composable, and generates types directly from schemas — removing the worst kind of duplication.
TypeScript kategorisinde daha fazlası
On the Templately admin we had four different ID types passed around as strings — TemplateId, CategoryId, UserId, OrganizationId — and the bugs that came from mixing them were hard to spot in code review. Branded types fixed that. Notes on the pattern and when it actually pays off.
A handful of TypeScript 5.x features changed how I write code day-to-day on the Templately admin and sapan.dev. Notes on the ones I reach for constantly — `satisfies`, `const` type parameters, `using` — and the ones I have not yet found a real use for.
On TubeOnAI, the original load/error/data form state was three independent booleans — and we kept hitting bugs where data was set AND loading was true. Switched to a single discriminated union and the entire class of bugs went away. Notes on the pattern that changed how I model state.