I am trying to set up the Content Collections
for my blog in Astro. I almost immediately ran into problems with draft posts: they were missing some schema fields, and Zod didn't like that.
Digging into Zod's docs, I came up with z.discriminatedUnion
, so the schema for "drafts" will be much looser than for published posts:
// src/content/config.ts
// (simplified example)
const publishedPostSchema = z.object({
title: z.string(),
author: z.enum(["John Doe", "Jane Doe"]),
dateCreated: z.string().transform((str) => new Date(str)),
description: z.string(),
draft: z.literal(null),
});
const draftPostSchema = z.object({
title: z.string().nullable(),
author: z.enum(["John Doe", "Jane Doe"]).nullable(),
dateCreated: z
.string()
.transform((str) => new Date(str))
.nullable(),
draft: z.literal(true),
});
const exampleBlogCollection = defineCollection({
schema: z.discriminatedUnion("draft", [publishedPostSchema, draftPostSchema]),
});
export const collections = {
blog: exampleBlogCollection,
};
This works, but I still have two minor issues:
1. This forces me to have a literal null
"draft" field on all published posts. I would prefer to enable null
, false
and undefined
values, but z.discriminatedUnion
accepts only literals on the discriminatory field. Is there a workaround to that?
2. The resulting type is a discriminated union, but it still needs a lot of work to ensure the TypeScript tooling, that what am I passing down is not nullable:
This does not work (TypeScript errors):
const allPosts = await getCollection('blog')
const publishedPosts = rawPosts.filter(post => post.data.draft === null)
publishedPosts .sort(
(a, b) =>
+(b.data.dateCreated) - +(a.data.dateCreated) // TS error: `dateCreated` can be `null`…
);
This does work, but… it's kind of verbose:
const allPosts = await getCollection('blog')
type PublishedPost = CollectionEntry<"blog"> & { data: { draft: null }}
function isPublishedPost(post: typeof allPosts[number]): post is PublishedPost {
return post.data.draft === null
}
const publishedPosts = allPosts.filter(isPublishedPost)
publishedPosts .sort(
(a, b) =>
+(b.data.dateCreated) - +(a.data.dateCreated) // O.K.
);
Is this the "expected" way to deal with schemas, or am I missing something important?