Welcome back!
In the previous chapter, Runtime Schema Validation, we built a "Bouncer" that stops bad data (like empty text) from entering our app. Before that, in Task Lifecycle State, we defined the "Traffic Light" rules for task progress.
Now, we bring these pieces together. We need to define the Whole Package. We need to define the Todo Entity.
Imagine a busy bureaucratic office. If you want to get anything done, you can't just scribble a request on a napkin. You must fill out a Standardized Form.
Why?
In our Todo application, the "Todo Entity" is that Standardized Form. It serves as the single source of truth for what a "Task" actually is. It ensures that every part of our systemβfrom the screen the user sees to the logic behind the scenesβspeaks the exact same language.
Our goal is to create a blueprint so that every time we create a task, it looks exactly like this:
If we didn't have this definition, one developer might name the text content, while another names it title, and the app would break.
TodoItemSchemaWe define this entity using Zod (our schema tool). We group individual rules into one big object definition.
Our Todo Entity is made of three specific fields.
string): The actual text description.TodoStatusSchema): This reuses the logic we built in Chapter 1. It enforces pending, in_progress, or completed.string): An ID used by the user interface to know which form is currently open.When we define the entity in code, we are essentially creating a mold. Any data poured into our app must fit this mold.
Here is what a valid Todo Entity looks like in JavaScript:
const validTask = {
content: "Buy groceries",
status: "pending",
activeForm: "main-list"
};
console.log("This fits the blueprint!");
Here is an object that fails to be a Todo Entity. It is missing the status and activeForm.
const invalidTask = {
content: "Buy groceries"
// Missing 'status'!
// Missing 'activeForm'!
};
// The system would reject this because
// it doesn't match the Entity Definition.
How does the system use this definition? Think of it as a quality control scanner in a factory.
Let's look at types.ts to see how we define this blueprint. We use z.object to bundle our requirements together.
We wrap this in lazySchema (which we will explain in Lazy Evaluation Pattern), but the focus here is the object structure.
// types.ts
import { z } from 'zod/v4'
import { lazySchema } from '../lazySchema.js'
// We combine our rules into one object
export const TodoItemSchema = lazySchema(() =>
z.object({
content: z.string().min(1, 'Content cannot be empty'),
status: TodoStatusSchema(), // Reusing Chapter 1 logic!
activeForm: z.string().min(1, 'Active form cannot be empty'),
}),
)
Walkthrough:
z.object({...}): This creates the "Standardized Form." It dictates that the data must be an object with keys and values.content: We define that every task MUST have text.status: TodoStatusSchema(): This is the power of composition! Instead of rewriting the "Traffic Light" rules, we simply refer to the concept we built in Task Lifecycle State.activeForm: We ensure the system tracks which form ID this task belongs to.Once we have the Schema (the runtime checker), we usually want a TypeScript type (for coding help).
// types.ts
// This creates a TypeScript type called "TodoItem"
// based exactly on the rules above.
export type TodoItem = z.infer<ReturnType<typeof TodoItemSchema>>
Note: The magic of how z.infer turns our validation rules into TypeScript code is the main topic of our next chapter, Type Inference Bridge.
In this chapter, we learned:
content, status, and activeForm, we prevent "missing data" bugs.TodoStatusSchema.We now have a complete definition of our data. But there is a gap: We have a Zod Schema (for JavaScript runtime) and we have TypeScript Code. How do we keep them perfectly in sync without writing everything twice?
Find out in the next chapter.
Next Chapter: Type Inference Bridge
Generated by Code IQ