Description
Assume we have the following set of TypeScript interfaces:
// Indexed type for type-specific fields
interface RichTextMap {
text?: { text: string };
equation?: { equation: string };
mention?: { mention: string };
}
// Main RichText interface with indexed types
interface RichTextBase {
type: "text" | "mention" | "equation";
annotations: any;
plain_text: string;
href: string | null;
}
type RichText = RichTextBase & (RichTextMap[RichTextBase["type"]] & {});
The RichText
type should allow the objects as follows:
const text = {
type: "text",
annotations: {},
plain_text: "",
href: null,
text: { text: "" }
}
const mention = {
type: "mention",
annotations: {},
plain_text: "",
href: null,
mention: { mention: "" }
}
Problem Statement
I need to define zod
schema for mentioned objects.
My understanding of zod
is yet to improve, but so far I've managed to come up with the following solution:
const RichTextSchema = z
.object({
type: z.enum(["text", "mention", "equation"]),
annotations: z.object({}).passthrough(),
plain_text: z.string(),
href: z.string().url().nullable(),
});
const TextRichTextSchema = RichTextSchema.extend({
text: z.object({ text: z.string() }).optional(),
})
const MentionRichTextSchema = RichTextSchema.extend({
mention: z.object({ mention: z.string() }).optional(),
})
const EquationRichTextSchema = RichTextSchema.extend({
equation: z.object({ equation: z.string() }).optional(),
})
Now, the problem with this approach is that I'm creating three different schemas and will have to figure out which one to call at runtime. While the interface type is generic enough to fit different objects under a single type.
Question: I wonder if there is a way to change the RichTextSchema
to be able to validate against the aforementioned interface types. Essentially, keeping only a single schema object that validates different objects from the example.
What you seem to be looking for is the exact use for case for discriminated unions in Zod.
Here is a example with literals based on your case:
import { z } from "zod";
const RichTextBaseSchema = z.object({
annotations: z.object({}).passthrough(),
plain_text: z.string(),
href: z.string().url().nullable(),
});
const RichTextSchema = z.discriminatedUnion("type", [
RichTextBaseSchema.extend({
type: z.literal("text"),
text: z.object({
text: z.string()
}),
}),
RichTextBaseSchema.extend({
type: z.literal("mention"),
mention: z.object({
mention: z.string()
}),
}),
RichTextBaseSchema.extend({
type: z.literal("equation"),
equation: z.object({
equation: z.string()
}),
}),
]);
Or if you want to keep the enum:
import { z } from "zod";
const RichTextType = z.enum(["text", "mention", "equation"]);
const RichTextBaseSchema = z.object({
annotations: z.object({}).passthrough(),
plain_text: z.string(),
href: z.string().url().nullable(),
});
const RichTextSchema = z.discriminatedUnion("type", [
RichTextBaseSchema.extend({
type: z.literal(RichTextType.enum.text),
text: z.object({
text: z.string()
}),
}),
RichTextBaseSchema.extend({
type: z.literal(RichTextType.enum.mention),
mention: z.object({
mention: z.string()
}),
}),
RichTextBaseSchema.extend({
type: z.literal(RichTextType.enum.equation),
equation: z.object({
equation: z.string()
}),
}),
]);