0Pricing
TypeScript Academy · Lesson

Composing and Refining Schemas

Build complex schemas with refine, transform, and unions.

Composing and Refining Schemas is a free TypeScript Academy lesson on CoddyKit — lesson 4 of 4. You can read the complete lesson below for free — then practise it hands-on in the browser with a built-in code editor and a 24/7 AI tutor. It is part of the TypeScript Academy learning path, one of 4 lessons in the course, and your progress syncs across the web and the CoddyKit app.

Schemas Compose

Zod schemas are composable building blocks. You can refine, transform, extend, and combine them to model complex validation rules.

import { z } from "zod";
const base = z.object({ name: z.string() });
// We will add rules and combine schemas from here.

Custom Rules With refine

.refine adds a custom validation predicate with an error message, for rules Zod has no built-in for.

import { z } from "zod";
const password = z.string().refine(
  (s) => s.length >= 8,
  { message: "Must be at least 8 characters" }
);
// password.parse("short") throws with that message.

Refining Across Fields

Apply .refine to an object to validate relationships between fields, like matching password confirmation.

import { z } from "zod";
const form = z.object({
  password: z.string(),
  confirm: z.string()
}).refine((d) => d.password === d.confirm, {
  message: "Passwords must match"
});
// Validates both fields together.

Transforming Values

.transform converts a validated value into another shape, running after validation succeeds.

import { z } from "zod";
const trimmed = z.string().transform((s) => s.trim());
const result = trimmed.parse("  hi  "); // "hi"

Optional Fields

.optional() makes a field allowed to be missing, reflected in both validation and the inferred type.

import { z } from "zod";
const schema = z.object({
  name: z.string(),
  bio: z.string().optional()
});
// bio may be omitted.

Default Values

.default supplies a value when the input is missing, so the parsed result always has the field filled in.

import { z } from "zod";
const schema = z.object({
  retries: z.number().default(3)
});
const r = schema.parse({}); // { retries: 3 }

Union Schemas

z.union accepts a value matching any one of several schemas, the Zod equivalent of a TypeScript union.

import { z } from "zod";
const id = z.union([z.string(), z.number()]);
// id.parse("abc") and id.parse(123) both succeed.

Discriminated Unions in Zod

z.discriminatedUnion validates tagged unions efficiently by switching on a discriminant field.

import { z } from "zod";
const shape = z.discriminatedUnion("kind", [
  z.object({ kind: z.literal("circle"), radius: z.number() }),
  z.object({ kind: z.literal("square"), side: z.number() })
]);
// Picks the right member by kind.

Merging With extend

.extend adds fields to an existing object schema, letting you build larger schemas from smaller ones.

import { z } from "zod";
const base = z.object({ id: z.number() });
const user = base.extend({ name: z.string() });
// user validates { id: number; name: string }

Combining Object Schemas

You can also merge two object schemas with .merge, combining all their fields into one schema.

import { z } from "zod";
const a = z.object({ x: z.number() });
const b = z.object({ y: z.string() });
const combined = a.merge(b);
// combined validates { x: number; y: string }

Chaining It All Together

Refinements, transforms, defaults, and extensions chain fluently to express rich validation in a single readable schema.

import { z } from "zod";
const signup = z.object({
  email: z.string().refine(s => s.includes("@"), { message: "Invalid email" }),
  age: z.number().default(18)
}).extend({
  newsletter: z.boolean().optional()
});
// One schema, many rules.

Quick Check: Composing Schemas

Test your understanding of composing and refining schemas.

Recap: Composing and Refining Schemas

You learned to add custom rules with .refine, reshape values with .transform, handle missing data with .optional and .default, combine alternatives with z.union, and grow schemas with .extend and .merge.

import { z } from "zod";
const schema = z.object({ name: z.string() })
  .extend({ age: z.number().default(0) })
  .refine(d => d.name.length > 0, { message: "Name required" });
// Composed validation in one place.

Frequently asked questions

Is the “Composing and Refining Schemas” lesson free?

Yes — the full text of “Composing and Refining Schemas” is free to read here on the web, and the TypeScript Academy course includes 4 lessons in total. To practise it interactively (a built-in code editor and a 24/7 AI tutor) and unlock the rest of the TypeScript Academy course, upgrade to CoddyKit PRO.

What will I learn in “Composing and Refining Schemas”?

Build complex schemas with refine, transform, and unions. You practise TypeScript Academy with hands-on code you run directly in the browser, and a 24/7 AI tutor answers your questions as you work through the lesson.

Do I need any experience to start TypeScript Academy?

No prior experience is required. TypeScript Academy on CoddyKit is structured for beginners through advanced learners; this is — lesson 4 of 4, so you can start here or from the beginning and move at your own pace.

How long does the “Composing and Refining Schemas” lesson take?

Most CoddyKit lessons take about 5–10 minutes. Each one is bite-sized and interactive, so you make steady progress and pick up exactly where you left off across the web and the app.

Can I write and run code in this TypeScript Academy lesson?

Yes. Every TypeScript Academy lesson includes a built-in code editor, so you write and run real code right in your browser and get instant AI feedback — no local setup required.

All lessons in this course

  1. Zod Schema Basics
  2. Inferring Types from Schemas
  3. parse vs safeParse
  4. Composing and Refining Schemas
← Back to TypeScript Academy