Skill 05 · Sanity Best Practices
Subchapter 5.23
references/typegen.mdMarkdown6 KBView on GitHub
Sanity TypeGen generates TypeScript types from your schema and GROQ queries. Types can be generated automatically or manually.
Enable in sanity.cli.ts — types regenerate during sanity dev and sanity build:
// sanity.cli.ts
import { defineCliConfig } from 'sanity/cli'
export default defineCliConfig({
typegen: {
enabled: true,
},
})Run the extract + generate cycle whenever schema or queries change:
npx sanity schemas extract --force && npx sanity typegen generateIf your frontend is in a separate repo from the Studio, use watch mode:
npx sanity typegen generate --watchFor manual workflows, implement a single script:
package.json:
"scripts": {
"typegen": "sanity schemas extract --force && sanity typegen generate"
}Option A: Commit generated types (Recommended for most teams)
git pullOption B: Generate in CI (Recommended for larger teams)
Add to .gitignore:
# Sanity TypeGen (generated)
sanity.types.ts
schema.jsonThen ensure CI runs typegen before build:
# Example GitHub Actions
- run: npm run typegen
- run: npm run buildNote:
sanity-typegen.jsonis deprecated. Move your configuration tosanity.cli.ts.
// sanity.cli.ts
import { defineCliConfig } from 'sanity/cli'
export default defineCliConfig({
typegen: {
enabled: true, // Auto-generate during sanity dev/build
path: "./src/**/*.{ts,tsx,js,jsx,astro,svelte,vue}", // Glob to find queries
schema: "schema.json", // Schema file from extract
generates: "./sanity.types.ts", // Output file
overloadClientMethods: true, // Auto-type client.fetch() calls
},
})Monorepo (recommended) (Studio in studio/, Frontend in web/ — same config works under apps/):
export default defineCliConfig({
typegen: {
path: "../web/src/**/*.{ts,tsx,js,jsx}",
schema: "schema.json",
generates: "../web/sanity.types.ts",
},
})Single Repo / Embedded Studio (legacy): Use defaults — no extra config needed.
Separate Repos:
Use --watch mode in your frontend: sanity typegen generate --watch
With overloadClientMethods: true (default), client.fetch() automatically returns typed results when you use defineQuery:
import { defineQuery } from "groq";
import { createClient } from "@sanity/client";
const client = createClient({...});
const POSTS_QUERY = defineQuery(`*[_type == "post"]{ title, slug }`);
// Return type is automatically inferred — no manual type import needed!
const posts = await client.fetch(POSTS_QUERY);You can also import generated types directly:
import { defineQuery } from "groq";
// Next.js re-exports defineQuery for convenience:
// import { defineQuery } from "next-sanity";
const AUTHOR_QUERY = defineQuery(`*[_type == "author" && slug.current == $slug][0]{ name, bio }`);
import type { AUTHOR_QUERY_RESULT } from "@/sanity.types";
export default function Author({ data }: { data: AUTHOR_QUERY_RESULT }) {
return <h1>{data.name}</h1>
}Use --enforce-required-fields during extraction to translate validation: rule => rule.required() into non-optional types:
npx sanity schemas extract --force --enforce-required-fields
npx sanity typegen generateWarning: If you use draft previews, fields may still be
undefinedeven with required validation, since drafts can be in an invalid state.
TypeGen provides utilities for working with complex types:
import type { Get, FilterByType } from 'sanity'
import type { Page, PageBuilder } from './sanity.types'
// Extract deeply nested type (up to 20 levels)
type HeroSection = Get<Page, 'sections', number, 'hero'>
// Filter specific types from unions using _type discriminator
type HeroBlock = FilterByType<PageBuilder, 'hero'>All queries must have unique variable names. Duplicate names across files will cause TypeGen to silently overwrite types. Use descriptive, scoped names:
// Unique names
const POSTS_INDEX_QUERY = defineQuery(`*[_type == "post"]{ title }`)
const POST_DETAIL_QUERY = defineQuery(`*[_type == "post" && slug.current == $slug][0]`)
// Duplicate names will conflict
const QUERY = defineQuery(`*[_type == "post"]`) // file-a.ts
const QUERY = defineQuery(`*[_type == "author"]`) // file-b.ts — overwrites!Queries must be assigned to a variable using groq or defineQuery:
// Works — groq template tag
const query = groq`*[_type == "post"]`
// Works — defineQuery
const query = defineQuery(`*[_type == "post"]`)
// Won't work — inline query
await client.fetch(groq`*[_type == "post"]`)TypeGen parses queries from: .ts, .tsx, .js, .jsx, .astro, .svelte, .vue
Ensure sanity.types.ts is included in your tsconfig.json‘s include array. If your config restricts includes (e.g., ["src/**/*"]) and the types file is at the project root, TypeScript won’t pick up the generated types:
{
"include": ["src/**/*", "sanity.types.ts"]
}Add @sanity-typegen-ignore in a comment before a query to skip type generation:
// @sanity-typegen-ignore
const debugQuery = groq`*[_type == "debug"]`