diff --git a/CHANGELOG.md b/CHANGELOG.md index 368e482..f927500 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,7 +9,6 @@ `` now infers the shape of every component handler from the `value` prop. When you pass a value typed by [Sanity TypeGen](https://www.sanity.io/docs/apis-and-sdks/sanity-typegen), `components.types`, `components.marks`, `components.block`, `components.list`, and `components.listItem` all receive precise `value` props for the exact content the query returned. Three new utility types ship with this feature: - - `InferComponents` - same inference as the inline `components` prop, for hoisting components out of JSX. - `InferStrictComponents` - strict variant that requires a handler for every inferred custom type, mark, block style, and list style, and rejects handlers that aren't in the schema (and therefore not visible to TypeGen). - `InferValue` - derives a Portable Text array value type from any TypeGen query result type, useful for re-usable wrapper components. @@ -20,33 +19,28 @@ ```ts // sanity.config.ts - import { - defineArrayMember, - defineConfig, - defineField, - defineType, - } from "sanity"; + import {defineArrayMember, defineConfig, defineField, defineType} from 'sanity' export default defineConfig({ - name: "default", - projectId: "abc123", - dataset: "production", + name: 'default', + projectId: 'abc123', + dataset: 'production', schema: { types: [ defineType({ - name: "post", - type: "document", + name: 'post', + type: 'document', fields: [ - defineField({ name: "title", type: "string" }), + defineField({name: 'title', type: 'string'}), defineField({ - name: "content", - type: "array", + name: 'content', + type: 'array', of: [ - defineArrayMember({ type: "block" }), + defineArrayMember({type: 'block'}), defineArrayMember({ - type: "image", - options: { hotspot: true }, - fields: [defineField({ name: "alt", type: "string" })], + type: 'image', + options: {hotspot: true}, + fields: [defineField({name: 'alt', type: 'string'})], }), ], }), @@ -54,7 +48,7 @@ }), ], }, - }); + }) ``` #### Before: hand-typing handlers @@ -63,26 +57,24 @@ ```tsx // app/[slug]/page.tsx - import { createClient } from "@sanity/client"; - import { createImageUrlBuilder } from "@sanity/image-url"; - import { PortableText } from "@portabletext/react"; - import { defineQuery } from "groq"; + import {createClient} from '@sanity/client' + import {createImageUrlBuilder} from '@sanity/image-url' + import {PortableText} from '@portabletext/react' + import {defineQuery} from 'groq' const client = createClient({ - projectId: "abc123", - dataset: "production", + projectId: 'abc123', + dataset: 'production', useCdn: true, - apiVersion: "2026-05-04", - }); - const builder = createImageUrlBuilder(client); + apiVersion: '2026-05-04', + }) + const builder = createImageUrlBuilder(client) - export default async function Page({ slug }: { slug: string }) { - const query = defineQuery( - `*[_type == "post" && slug.current == $slug][0]{title,content}` - ); - const data = await client.fetch(query, { slug }); + export default async function Page({slug}: {slug: string}) { + const query = defineQuery(`*[_type == "post" && slug.current == $slug][0]{title,content}`) + const data = await client.fetch(query, {slug}) - if (!data) return notFound(); + if (!data) return notFound() return (
@@ -95,37 +87,35 @@ }: { value: { asset?: { - _ref: string; - _type: "reference"; - _weak?: boolean; - }; + _ref: string + _type: 'reference' + _weak?: boolean + } hotspot?: { - _type: "sanity.imageHotspot"; - x?: number; - y?: number; - height?: number; - width?: number; - }; + _type: 'sanity.imageHotspot' + x?: number + y?: number + height?: number + width?: number + } crop?: { - _type: "sanity.imageCrop"; - top?: number; - bottom?: number; - left?: number; - right?: number; - }; - alt?: string; - _type: "image"; - _key: string; - }; - }) => ( - {value.alt - ), + _type: 'sanity.imageCrop' + top?: number + bottom?: number + left?: number + right?: number + } + alt?: string + _type: 'image' + _key: string + } + }) => {value.alt, }, }} value={data.content} />
- ); + ) } ``` @@ -135,26 +125,24 @@ ```tsx // app/[slug]/page.tsx - import { createClient } from "@sanity/client"; - import { createImageUrlBuilder } from "@sanity/image-url"; - import { PortableText } from "@portabletext/react"; - import { defineQuery } from "groq"; + import {createClient} from '@sanity/client' + import {createImageUrlBuilder} from '@sanity/image-url' + import {PortableText} from '@portabletext/react' + import {defineQuery} from 'groq' const client = createClient({ - projectId: "abc123", - dataset: "production", + projectId: 'abc123', + dataset: 'production', useCdn: true, - apiVersion: "2026-05-04", - }); - const builder = createImageUrlBuilder(client); + apiVersion: '2026-05-04', + }) + const builder = createImageUrlBuilder(client) - export default async function Page({ slug }: { slug: string }) { - const query = defineQuery( - `*[_type == "post" && slug.current == $slug][0]{title,content}` - ); - const data = await client.fetch(query, { slug }); + export default async function Page({slug}: {slug: string}) { + const query = defineQuery(`*[_type == "post" && slug.current == $slug][0]{title,content}`) + const data = await client.fetch(query, {slug}) - if (!data) return notFound(); + if (!data) return notFound() return (
@@ -163,15 +151,13 @@ components={{ types: { // value is fully typed from the query result, no annotation needed - image: ({ value }) => ( - {value.alt - ), + image: ({value}) => {value.alt, }, }} value={data.content} />
- ); + ) } ``` @@ -181,41 +167,37 @@ ```tsx // app/[slug]/page.tsx - import { createClient } from "@sanity/client"; - import { createImageUrlBuilder } from "@sanity/image-url"; - import { PortableText, type InferComponents } from "@portabletext/react"; - import { defineQuery } from "groq"; + import {createClient} from '@sanity/client' + import {createImageUrlBuilder} from '@sanity/image-url' + import {PortableText, type InferComponents} from '@portabletext/react' + import {defineQuery} from 'groq' const client = createClient({ - projectId: "abc123", - dataset: "production", + projectId: 'abc123', + dataset: 'production', useCdn: true, - apiVersion: "2026-05-04", - }); - const builder = createImageUrlBuilder(client); + apiVersion: '2026-05-04', + }) + const builder = createImageUrlBuilder(client) - export default async function Page({ slug }: { slug: string }) { - const query = defineQuery( - `*[_type == "post" && slug.current == $slug][0]{title,content}` - ); - const data = await client.fetch(query, { slug }); + export default async function Page({slug}: {slug: string}) { + const query = defineQuery(`*[_type == "post" && slug.current == $slug][0]{title,content}`) + const data = await client.fetch(query, {slug}) - if (!data) return notFound(); + if (!data) return notFound() const components = { types: { - image: ({ value }) => ( - {value.alt - ), + image: ({value}) => {value.alt, }, - } satisfies InferComponents; + } satisfies InferComponents return (

{data.title}

- ); + ) } ``` @@ -225,56 +207,46 @@ ```tsx // app/[slug]/page.tsx - import { createClient, type SanityQueries } from "@sanity/client"; - import { createImageUrlBuilder } from "@sanity/image-url"; - import { - PortableText, - type InferStrictComponents, - type InferValue, - } from "@portabletext/react"; - import { defineQuery } from "groq"; + import {createClient, type SanityQueries} from '@sanity/client' + import {createImageUrlBuilder} from '@sanity/image-url' + import {PortableText, type InferStrictComponents, type InferValue} from '@portabletext/react' + import {defineQuery} from 'groq' const client = createClient({ - projectId: "abc123", - dataset: "production", + projectId: 'abc123', + dataset: 'production', useCdn: true, - apiVersion: "2026-05-04", - }); - const builder = createImageUrlBuilder(client); + apiVersion: '2026-05-04', + }) + const builder = createImageUrlBuilder(client) // Array value type for every Portable Text item shape across all registered queries. - type PortableTextValue = InferValue; + type PortableTextValue = InferValue - function CustomPortableText({ value }: { value: PortableTextValue }) { + function CustomPortableText({value}: {value: PortableTextValue}) { const components = { types: { - image: ({ value }) => ( - {value.alt - ), + image: ({value}) => {value.alt, }, - } satisfies InferStrictComponents; + } satisfies InferStrictComponents // ^ TypeScript errors when the schema gains a custom type, mark, or list // style without a matching handler defined here - return ; + return } - export default async function Page({ slug }: { slug: string }) { - const query = defineQuery( - `*[_type == "post" && slug.current == $slug][0]{title,content}` - ); - const data = await client.fetch(query, { slug }); + export default async function Page({slug}: {slug: string}) { + const query = defineQuery(`*[_type == "post" && slug.current == $slug][0]{title,content}`) + const data = await client.fetch(query, {slug}) - if (!data) return notFound(); + if (!data) return notFound() return (

{data.title}

- {Array.isArray(data.content) && ( - - )} + {Array.isArray(data.content) && }
- ); + ) } ```