Sanity
Learn
CoursesTyped content with Sanity TypeGenAdding the new type to your front end code
Track
Sanity Developer Certification

Typed content with Sanity TypeGen

Lesson
6

Adding the new type to your front end code

Log in to mark your progress for each Lesson and Task

Open the file with the EVENTS_QUERY variable and find where you are doing the client.fetch() call. Replace the more generic SanityDocument[] type with your new type. Your code editor might support automatically importing the TypeScript type if you start typing out the name in between the <>:

import { EVENTS_QUERYResult } from "@/sanity/types"
//... code
const events = await client.fetch<EVENTS_QUERYResult>(EVENTS_QUERY)

With this change, you might already get some new TypeScript errors in this file. For example, if you have followed the Day One course, you’ll find an error telling you that the event slug.current might be null.

Strictly, slug.current can’t ever be null in practice because of the defined(slug.current) filter. Unfortunately, Sanity TypeGen doesn’t support null comparisons in filters, so you’ll have to check against slug being null.

Tweak your code like this to remove the TypeScript errors:

./src/app/page.tsx
// src/app/page.tsx
import Link from "next/link"
import { groq } from "next-sanity"
import { client } from "@/sanity/client"
import { EVENTS_QUERYResult } from "@/sanity/types"
const EVENTS_QUERY = groq`*[_type == "event" && slug.current != null]{_id, name, slug, date}|order(date desc)`
// Display Sanity content on the page
export default async function IndexPage() {
const events = await client.fetch<EVENTS_QUERYResult>(EVENTS_QUERY)
return (
<main className="flex min-h-screen flex-col items-center justify-between p-24">
<h2 className="text-3xl font-bold tracking-tighter sm:text-4xl md:text-5xl text-center mb-8">
Events
</h2>
<ul className="grid grid-cols-1 gap-6 lg:grid-cols-2 xl:grid-cols-3 lg:gap-8">
{events.map((event) => (
<li
className="event-card bg-white dark:bg-gray-950 p-4 rounded-lg shadow-md"
key={event._id}
>
{event.slug && (
<Link
className="hover:underline"
href={`/events/${event.slug.current}`}
>
<h2 className="text-xl font-semibold">{event?.name}</h2>
{event?.date && (
<p className="text-gray-500 dark:text-gray-400">
{new Date(event?.date).toLocaleDateString()}
</p>
)}
</Link>
)}
</li>
))}
</ul>
</main>
)
}

Now, you can repeat the steps from above in the individual event route. Search and find the EVENT_QUERY in your front end project and add the groq template literal to it:

const EVENT_QUERY = groq`*[
_type == "event" &&
slug.current == $slug
][0]{
...,
slug,
headline->,
venue->
}`

Run sanity typegen generate in your studio folder again. The output should say that it has made types for 2 GROQ queries.

Now, replace the SanityDocumentLike type with the new EVENT_QUERYResult type:

const event = await client.fetch<EVENT_QUERYResult>(EVENT_QUERY, params)

As with the other file, you might have some TypeScript error in your code editor. Primarily related to values potentially being null. You solve this by adjusting your code with null (or undefined) tests. This “defensive coding” is generally a better pattern, especially if you plan to implement content previews later where values are often lacking. Here is what the adjusted code looks like with the Next.js front end, which will be pretty similar to other frameworks:

import { PortableText } from "@portabletext/react"
import { groq } from "next-sanity"
import imageUrlBuilder from "@sanity/image-url"
import { SanityImageSource } from "@sanity/image-url/lib/types/types"
import { client } from "@/sanity/client"
import Link from "next/link"
import { EVENT_QUERYResult } from "@/sanity/types"
const EVENT_QUERY = groq`*[
_type == "event" &&
slug.current == $slug
][0]{
...,
slug,
headline->,
venue->
}`
const { projectId, dataset } = client.config()
export const urlFor = (source: SanityImageSource) =>
projectId && dataset
? imageUrlBuilder({ projectId, dataset }).image(source)
: null
export default async function EventPage({
params,
}: {
params: { slug: string }
}) {
const event = await client.fetch<EVENT_QUERYResult>(EVENT_QUERY, params)
const {
name,
date,
headline,
image,
details,
format,
doorsOpen,
venue,
tickets,
} = event || {}
const eventImageUrl = image?.asset ? urlFor(image)?.url() : null
const artistImageUrl = headline?.photo?.asset
? urlFor(headline.photo)?.url()
: null
const eventDate = date && new Date(date).toDateString()
const eventTime = date && new Date(date).toLocaleTimeString()
const doorsOpenTime =
date &&
doorsOpen &&
new Date(new Date(date).getTime() + doorsOpen * 60000).toLocaleTimeString()
return (
<main className="w-full min-h-screen py-12 md:py-24 lg:py-32">
<div className="container px-4 md:px-6 mx-auto">
<div className="mb-4">
<Link href="/">← Back to events</Link>
</div>
<div className="grid items-top gap-6 lg:grid-cols-[1fr_500px] lg:gap-12 xl:grid-cols-[1fr_550px]">
{(eventImageUrl || artistImageUrl) && (
// eslint-disable-next-line @next/next/no-img-element
<img
alt="Image"
className="mx-auto aspect-video overflow-hidden rounded-xl object-cover object-center sm:w-full"
height="310"
src={eventImageUrl || artistImageUrl || ""}
width="550"
/>
)}
<div className="flex flex-col justify-center space-y-4">
<div className="space-y-2">
{format && (
<div className="inline-block rounded-lg bg-gray-100 px-3 py-1 text-sm dark:bg-gray-800 capitalize">
{format.replace("-", " ")}
</div>
)}
{name && (
<h1 className="text-3xl font-bold tracking-tighter sm:text-5xl">
{name}
</h1>
)}
{headline?.name && (
<dl className="grid grid-cols-2 gap-1 text-sm font-medium sm:gap-2 lg:text-base">
<div className="flex items-start">
<dt className="sr-only">Artist</dt>
<dd className="font-semibold">Artist</dd>
</div>
<div className="grid gap-1">
<dt>{headline?.name}</dt>
</div>
</dl>
)}
<dl className="grid grid-cols-2 gap-1 text-sm font-medium sm:gap-2 lg:text-base">
<div className="flex items-start">
<dt className="sr-only">Date</dt>
<dd className="font-semibold">Date</dd>
</div>
<div className="grid gap-1">
{eventDate && <dt>{eventDate}</dt>}
{eventTime && <dt>{eventTime}</dt>}
</div>
</dl>
{doorsOpenTime && (
<dl className="grid grid-cols-2 gap-1 text-sm font-medium sm:gap-2 lg:text-base">
<div className="flex items-start">
<dt className="sr-only">Doors Open</dt>
<dd className="font-semibold">Doors Open</dd>
</div>
<div className="grid gap-1">
<dt>Doors Open</dt>
<dt>{doorsOpenTime}</dt>
</div>
</dl>
)}
<dl className="grid grid-cols-2 gap-1 text-sm font-medium sm:gap-2 lg:text-base">
<div className="flex items-start">
<dt className="sr-only">Venue</dt>
<dd className="font-semibold">Venue</dd>
</div>
<div className="grid gap-1">
<dt>{venue?.name}</dt>
<dt>
{venue?.city}, {venue?.country}
</dt>
</div>
</dl>
</div>
{details && details.length > 0 && (
<div className="prose max-w-none">
<PortableText value={details} />
</div>
)}
{tickets && (
<div className="flex gap-4">
<a
className="inline-flex h-10 items-center justify-center rounded-md bg-gray-900 w-1/2 text-sm font-medium text-gray-50 shadow transition-colors hover:bg-gray-900/90 focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-gray-950 disabled:pointer-events-none disabled:opacity-50 dark:bg-gray-50 dark:text-gray-900 dark:hover:bg-gray-50/90 dark:focus-visible:ring-gray-300"
href={tickets}
>
Buy Tickets
</a>
</div>
)}
</div>
</div>
</div>
</main>
)
}