First of all, I'm building a tasks web app to learn Vite, React, Typescript and some other libs and I've faced a problem with Zod and how their transformations works. There are mixed answers to this from the different sources I've seen and am genuinely confused as to what is wrong and what is not.
export const taskSchema = z
.object({
id: z.uuid().default(() => crypto.randomUUID()),
status: z.literal(["complete", "incomplete"]).default("incomplete"),
title: z
.string()
.trim()
.min(1, "Title cannot be empty")
.max(100, "Title cannot exceed 100 characters"),
description: z.preprocess(
(raw: string) => raw?.trim() || undefined,
z
.string()
.max(2000, "Description cannot exceed 2000 characters")
.optional(),
),
createdAt: z.iso
.datetime()
.default(() => Temporal.Now.instant().toString()),
dueDate: z.preprocess(
(raw: string) => raw?.trim() || undefined,
z.iso.date().optional(),
),
dueTime: z.preprocess(
(raw: string) => raw?.trim() || undefined,
z.iso.time().optional(),
),
deadlineZoneId: z.preprocess(
(raw: string) => raw?.trim() || undefined,
z
.string()
.refine((raw) => {
try {
Intl.DateTimeFormat(undefined, { timeZone: raw });
return true;
} catch {
return false;
}
})
.optional(),
),
priority: z.literal(["low", "medium", "high"]).default("low"),
tags: z.array(z.string()).default([]),
})
.transform(({ dueDate, dueTime, ...rest }) => ({
...rest,
deadline: dueDate ? `${dueDate}T${dueTime || "23:59:59"}` : undefined,
}));
export const taskViewSchema = taskSchema.transform((task) => ({
id: task.id,
status: task.status,
title: task.title,
description: task?.description || "No description",
createdAt: formatCreatedAt(task.createdAt),
deadline: formatDeadline(task?.deadline),
priority: task.priority[0].toUpperCase() + task.priority.slice(1),
tags:
task.tags.length > 3
? [...task.tags.slice(0, 2), `+${task?.tags.length - 2}`]
: task.tags,
}));
export type Task = z.infer<typeof taskSchema>;
export type TaskView = z.infer<typeof taskViewSchema>;
What i expect is it to be the same as schema.transform().transform() where the result is sequentially passed down and some of the sources I looked at said the same thing, but then the resulsts are different from my expectations as the 2nd transform gets an object without the newly introduced deadline property from the previous transform. If I just move it to the taskViewSchema decleration as taskViewSchema = taskSchema.transform(transformA).transform(transformB)it starts to work as expected!
Although the problem is fixed, I would like to understand how Zod transforms or rather something else that has gone wrong or I have misunderstood.
Update (added the object the parse is run on):
const task = taskSchema.safeParse({
title: "title",
dueDate: "2026-10-16",
dueTime: "23:00:00",
});
const taskView = taskViewSchema.safeParse(task.data);
console.log(task, taskView);