Welcome to the final chapter of our Todo tutorial series!
In the previous chapter, Type Inference Bridge, we learned how to automatically generate TypeScript types from our validation logic. Throughout that code, you might have noticed a mysterious helper function wrapping our definitions: lazySchema.
You might have asked: "Why can't we just write z.object(...) directly? Why do we need this wrapper?"
In this chapter, we will answer that question by exploring the Lazy Evaluation Pattern.
Imagine you are reading a complex Pop-Up Book.
If the book tried to keep all the pop-ups fully built while closed, the book would be 5 feet thick and impossible to carry!
In programming, we face a similar issue called Circular Dependencies (or the "Chicken and Egg" problem).
Imagine we want to upgrade our Todo app so that a Task can have "Sub-tasks".
Task contains a list of SubTasks.SubTask is just another Task.
If the computer tries to build the Task definition, it asks: "What is a SubTask?"
It goes to look at SubTask, which says: "I am a Task."
The computer gets confused about who to build first and crashes.
lazySchemaThe Lazy Evaluation Pattern solves this. Instead of building the schema immediately (Eager), we give the computer a set of instructions (a Function) on how to build it, but we tell it:
"Don't build this yet. Wait until I actually ask for it."
In our code, lazySchema is that instruction envelope.
Let's look at how we defined our TodoItemSchema in the previous chapters.
// types.ts
import { z } from 'zod/v4'
import { lazySchema } from '../lazySchema.js'
// We wrap the definition in a function: () => ...
export const TodoItemSchema = lazySchema(() =>
z.object({
// ... fields ...
status: TodoStatusSchema(), // We call this as a function!
}),
)
Key Differences:
lazySchema(...).() => z.object(...). This is the "Instruction Manual." The code inside the arrow function does not run when the file loads. It only runs later when needed.
Using a lazy schema is slightly different from using a normal variable. Because it is a function, we must call it with parentheses () to get the actual validator.
We define the schema using the arrow function syntax.
// Definition
const MyLazySchema = lazySchema(() =>
z.string()
);
When we want to check data (like we did in Runtime Schema Validation), we execute the function.
const input = "Hello World";
// Notice the parentheses: MyLazySchema()
const result = MyLazySchema().parse(input);
console.log(result); // "Hello World"
If we forgot the parenthesesβMyLazySchema.parse(input)βit would fail because MyLazySchema is just a function waiting to be called, not the validator itself.
What happens when our application starts versus when we validate data?
The program loads the file. It sees lazySchema, but it doesn't look inside. It just notes: "Okay, I have a recipe for a Todo Schema." This makes startup very fast.
When a user submits data, we finally "open the pop-up book."
Let's look at types.ts again to see how this pattern connects our different schemas.
We have a TodoItem (single task) and a TodoList (array of tasks).
// types.ts (Simplified)
// 1. Define the Item first
export const TodoItemSchema = lazySchema(() =>
z.object({ /* fields */ })
)
// 2. Define the List, which uses the Item
export const TodoListSchema = lazySchema(() =>
z.array(TodoItemSchema())
)
Explanation:
TodoListSchema needs to know what an Item looks like.z.array(...), we call TodoItemSchema().lazySchema, it doesn't matter strictly which one is defined first in the file execution order. They resolve each other only when the app actually runs validation.This makes our code Robust. We can move files around, split them up, or create circular references (like Sub-Tasks), and the application won't crash on startup.
In this final chapter, we learned:
lazySchema(() => ...) wrappers.Congratulations! You have completed the Todo project tutorial. You have built a rock-solid foundation for data management:
pending, completed).You now possess the tools to build applications that are type-safe, bug-resistant, and easy to maintain. Happy coding!
Generated by Code IQ