APIs and SDKs

Sanity TypeGen

How to use Sanity TypeGen tooling to generate TypeScript definitions from Sanity Studio schemas and GROQ queries.

If you use TypeScript for your frontend or web application, you can type the content you fetch from the Sanity Content Lake API. With the Sanity TypeGen tooling, you can generate type definitions from the schema types in your studio and from the results of your GROQ queries in your frontends and applications.

Typing your content is useful for:

  • Catching bugs and errors caused by wrongly handled data types, like forgetting to check for null or a undefined property
  • Autocomplete of fields available in the result of your GROQ query
  • Making it easier to refactor integration code when you make changes to the schema

This article covers the different aspects of type generation and walks you through workflows for different project structures: separate repositories, monorepos, and embedded studios.

Requirements

  • Sanity Studio v6.6.0 or later
  • A Sanity Studio project with a schema
  • GROQ queries assigned to variables and written with the groq template literal or defineQuery (from groq, next-sanity, or @sanity/sveltekit)

Overview

You can use Sanity TypeGen to generate types for your Sanity Studio schema and for the return value of a GROQ query run against documents made from that schema.

Types from your schema can be useful for cases where GROQ isn't used, such as Studio customization and schema change management.

The most common use case is generating types for GROQ queries. TypeGen works by "overlaying the schema types" over the GROQ query to determine what types the returned data will have.

Using GraphQL?

If you primarily use the Sanity GraphQL API, we recommend using established GraphQL TypeScript tooling, like GraphQL Code Generator. You can use your GraphQL API URL as the configuration setting for schema.

Minimal example

Sanity TypeGen needs to access a static representation of your Studio schema to generate types. You can use the sanity schema extract command to create the schema.json file the TypeGen command requires:

$ cd ~/my-studio-folder
$ npx sanity schema extract # outputs a `schema.json` file
✔ Extracted schema to /Users/dev/my-studio-folder/schema.json

$ npx sanity typegen generate
✔ Config loaded from sanity.cli.ts
✔ Schema loaded from ./schema.json
✔ Successfully generated types to ./sanity.types.ts in 225ms
  └─ 2 queries and 2 schema types
  └─ found queries in 1 file after evaluating 1 file
  └─ formatted the generated code with prettier

Types from schemas

Take this schema type for "event" documents:

Input

export const event = defineType({
  name: 'event',
  type: 'document',
  title: 'Event',
  fields: [
    defineField({
      name: 'name',
      type: 'string',
      title: 'Event name',
      validation: rule => rule.required()
    }),
    defineField({
      name: 'description',
      type: 'text',
      title: 'Event description'
    })
  ]
})

Generated types

export type Event = {
  _id: string;
  _type: 'event';
  _createdAt: string;
  _updatedAt: string;
  _rev: string;
  name?: string;
  description?: string
}

Supported schema types

Nearly all schema types and all permutations of schema types are supported:

  • Document types
  • Literal fields (boolean, string, text, number, date, dateTime)
  • Object types, including geopoint
  • Array of types
  • Portable Text (Block content)
  • References
  • Image and file assets

Unsupported schema that will be typed as unknown:

  • Cross-dataset references
  • Global document references

Supported schema features

Because studio schemas are defined in JavaScript, TypeGen can't statically generate TypeScript definitions from every configuration option. TypeGen supports the following schema configuration options.

Required field validation and non-optional fields

If you add validation: rule => rule.required() to a field, you might want to translate required rules into non-optional types depending on your use case. You do this by adding the --enforce-required-fields flag when extracting the schema:

$ npx sanity schema extract --enforce-required-fields
✔ Extracted schema to /Users/dev/my-studio-folder/schema.json with enforced required fields
$ npx sanity typegen generate
✔ ...

Gotcha

"Built-in" fields required for documents in the Sanity Content Lake will also be set to required in the TypeScript definition: _id, _type, _createdAt, _updatedAt, _rev.

Literals and options.list

The string schema type supports adding a list of predefined values in options.list. This gets generated into a literal type in the TypeScript definition. The generated types in this example come from a schema extracted with --enforce-required-fields, which is why name is non-optional:

Input

export const event = defineType({
  name: 'event',
  type: 'document',
  title: 'Event',
  fields: [
    defineField({
      name: 'name',
      type: 'string',
      title: 'Event name',
      validation: rule => rule.required()
    }),
    defineField({
      name: 'description',
      type: 'text',
      title: 'Event description'
    }),
    defineField({
      name: 'format',
      type: 'string',
      title: 'Event format',
      options: {
        list: ['in-person', 'virtual'],
        layout: 'radio',
      },
    }),
  ]
})

Generated types

export type Event = {
  _id: string;
  _type: 'event';
  _createdAt: string;
  _updatedAt: string;
  _rev: string;
  name: string;
  description?: string;
  format?: 'in-person' | 'virtual';
}

Types from GROQ queries

Sanity TypeGen can also generate TypeScript definitions for GROQ query results. This is useful since GROQ is a query language that lets you specify which fields to return in projections and re-shape that data to fit your needs.

The CLI command requires that a GROQ query is:

  • Assigned to a variable (it does not need to be exported)
  • Uses the groq template literal, or defineQuery, from the groq package (also exported by next-sanity)
  • Validates as a GROQ expression

The typegen command requires every query to be assigned to a variable whose name is unique across all files. Inline queries aren't assigned to a variable, so they aren't included in the generated types.

import {createClient} from '@sanity/client'
import groq, {defineQuery} from 'groq'

const client = createClient({
  projectId: '', // replace with your project ID
  dataset: 'production',
  apiVersion: '2026-10-28',
  useCdn: true,
})

// ✅ Will be included
async function getStuff() {
  const myUniquelyNamedQuery = groq`*[_type == 'post']{ slug, title }`
  const result = await client.fetch(myUniquelyNamedQuery)
  return result
}

async function getMoreStuff() {
  const myOtherUniquelyNamedQuery = defineQuery(`*[_type == 'post']{ slug, title }`)
  const result = await client.fetch(myOtherUniquelyNamedQuery)
  return result
}

// ❌ Will not be included
async function getInlineStuff() {
  const result = await client.fetch(groq`*[_type == 'post']{ slug, title }`)
  return result
}

Supported query patterns

TypeGen reads your source files without running them. To build each query, it follows every value interpolated into the template literal back to where that value is defined. TypeGen can resolve these values:

  • String, number, and Boolean literals, and variables that hold them
  • Other query strings and GROQ fragments, such as a shared projection
  • Object properties, with dot notation (DOC_TYPES.PAGE) or a string key (DOC_TYPES['PAGE']), including objects declared as const
  • Calls to arrow functions that return a template literal, when every argument is a literal
  • Values imported from other files in your project

TypeGen can't resolve these values:

  • Conditional (ternary) expressions
  • Strings joined with the + operator
  • Array elements, such as TYPES[0]
  • Function calls with a variable as an argument, such as byType(POST_TYPE)
  • Functions with a block body, including function declarations
  • Renamed imports, such as import {POST_TYPE as TYPE} or import {defineQuery as dq}
  • Values that only exist at runtime, such as environment variables

The following file shows queries TypeGen resolves and queries it can't:

import {defineQuery} from 'groq'

const DOC_TYPES = {PAGE: 'page', POST: 'post'} as const
const POST_TYPE = DOC_TYPES.POST
const isPage = true
const byType = (type: string) => `_type == "${type}"`

// ✅ Resolved
export const pageQuery = defineQuery(`*[_type == "${DOC_TYPES.PAGE}"]`)
export const postQuery = defineQuery(`*[${byType('post')}]`)

// ❌ Not resolved
export const typedQuery = defineQuery(`*[${byType(POST_TYPE)}]`)
export const pageOrPostQuery = defineQuery(`*[_type == "${isPage ? 'page' : 'post'}"]`)

When TypeGen can't resolve a query, it skips that query and prints an error. The error names the query variable and the file and, for most expressions, the line and column of the expression it couldn't resolve. Types for your other queries still generate:

$ npx sanity typegen generate
✖ Error while extracting query from variable 'typedQuery' in ./src/sanity.queries.ts: Could not find binding for node "type" in ./src/sanity.queries.ts:6:46
✖ Error while extracting query from variable 'pageOrPostQuery' in ./src/sanity.queries.ts: Unsupported expression type: ConditionalExpression in ./src/sanity.queries.ts:14:58
⚠ Encountered errors in 1 file while generating types
✔ Successfully generated types to ./sanity.types.ts in 324ms
  └─ 2 queries and 0 schema types

To fix a query TypeGen can't resolve, replace the value with a constant TypeGen can follow. For values that change at runtime, use a GROQ parameter such as $type in the query and pass the value when you fetch.

// ❌ TypeGen can't follow POST_TYPE into byType()
export const typedQuery = defineQuery(`*[${byType(POST_TYPE)}]`)

Supported GROQ features

Since GROQ is so versatile, we are still working on identifying edge cases, and some functions are not yet supported.

Unsupported expressions

Supported features:

  • Data types: Null, Boolean, Number, String, Array, Object
  • Selectors: Everything (*), this (@), attribute filters ([name == "string"]), parent (^)
  • Functions: coalesce(), dateTime::now(), global::now(), round(), upper(), lower(), select() (and =>), and array functions
  • Compounds: parentheses, traversals ([]), pipe function calls (|)
  • Operators: and (&&), or (||), not (!), equality (==, !=), comparison (<, <=, >, >=), plus (+), minus (-), unaries (+ -), star (*), slash (/), percent (%), star star (**)

Supported file types

TypeGen parses queries from multiple file types:

  • TypeScript (.ts, .tsx)
  • JavaScript (.js, .jsx, .mjs, .cjs)
  • Astro (.astro)
  • Svelte (.svelte)
  • Vue (.vue)

SvelteKit's defineQuery function is fully supported for automatic type inference.

Automatic Sanity client type inference

When you write your GROQ queries with defineQuery, the Sanity client automatically returns types when you pass the query to fetch, after you run sanity typegen generate.

Example: automatic type inference

import { defineQuery } from 'groq'

export const postsQuery = defineQuery(`*[_type == "event"]{name}`)

Gotcha

Opt out of automatic type inference

Ignoring individual queries

You can instruct the type generator to skip generating query types for individual queries by having @sanity-typegen-ignore in a leading comment before the query, similar to how ESLint and TypeScript can be instructed.

Example: ignore a query

Input

import groq from 'groq'
// import { groq } from 'next-sanity'

const postQuery = groq`*[_type == "event"]{name}`

const authorQuery = groq`*[_type == "author" && name == $name][0]{_type, name, description}`

// this query won't get generated types because of the instruction below
// @sanity-typegen-ignore
const anotherQuery = groq`*[_type == "another"][0]`

Generated types

// Variable: postQuery
// Query: *[_type == "event"]{name}
export type PostQueryResult = Array<{
  name: string | null
}>

// Variable: authorQuery
// Query: *[_type == "author" && name == $name][0]{_type, name, description}
export type AuthorQueryResult = {
  _type: 'author'
  name: string | null
  description: string | null
} | null

Type utilities

TypeGen provides utility types to help you work with generated types in complex schemas.

Get utility

The Get utility extracts deeply nested properties from your types, supporting up to 20 levels of nesting. This is especially useful for page builder schemas with complex nested structures.

import type { Get } from '@sanity/codegen'
import type { Page } from './sanity.types'

// Extract a deeply nested property type
type HeroImageAlt = Get<Page, 'hero', 'image', 'alt'>

// Use `number` to step into an array
type Section = Get<Page, 'sections', number>

This replaces verbose type manipulation with NonNullable and index access.

FilterByType utility

The FilterByType utility filters specific types from union types using the _type discriminator. This is useful when working with arrays of different block types.

import type { FilterByType } from '@sanity/codegen'
import type { PageBuilder } from './sanity.types'

// Extract only hero blocks from a union
type HeroBlock = FilterByType<PageBuilder, 'hero'>

// Works with multiple types
type ContentBlocks = FilterByType<PageBuilder, 'hero' | 'textBlock' | 'imageGallery'>

Type generation workflows

You generate types from your schemas and GROQ queries with the Sanity CLI, in two steps. First, you run sanity schema extract in the root folder of your studio project to extract the structure of your schemas into a JSON file. Then you run sanity typegen generate to generate type definitions from that JSON file.

Automatic type generation

Automatic type generation is the recommended workflow. Enable it in your sanity.cli.ts configuration, and types regenerate automatically during development.

// sanity.cli.ts
import { defineCliConfig } from 'sanity/cli'

export default defineCliConfig({
  typegen: {
    enabled: true,
  },
})

With enabled set to true, types regenerate automatically when you run sanity dev or sanity build. You can still use manual commands when needed.

Understanding configuration options

General workflow

  • Extract current schema with sanity schema extract
  • Generate types from schemas and queries with sanity typegen generate

Extracting Studio schema to schema.json

The first step towards generating type definitions based on your schemas is to extract the entire schema structure into a single JSON file for the typegen command to ingest.

👉 In your Sanity project root (wherever your sanity.config.ts lives), run the following CLI command:

$ npx sanity schema extract
✔ Extracted schema to /Users/dev/my-studio-folder/schema.json

# If you have multiple workspaces defined, specify which one to use:
$ npx sanity schema extract --workspace=commerce
✔ Extracted schema to /Users/dev/my-studio-folder/schema.json

The CLI picks up the schema definition from your project configuration and writes a representation of your complete schema structure to a new file named schema.json, unless you set another output path with the --path flag.

🔗 Learn how to override the default output file and more in the Schemas CLI command reference.

Generate sanity.types.ts from schema.json

After you extract your schema to schema.json with sanity schema extract, you can generate types.

👉 Still in your Sanity project root, run the following command in your CLI:

$ npx sanity typegen generate
✔ ...

The CLI reads the schema.json file created by sanity schema extract and creates a new file, by default named sanity.types.ts, containing all the type declarations for your schema and for any GROQ query found in the default source paths: ./src, ./app, and ./sanity.

The generate command can be configured by adding a property named typegen to your sanity.cli.ts/js containing a configuration object with the following shape:

import { defineCliConfig } from 'sanity/cli';

export default defineCliConfig({
  // ...rest of config
  typegen: {
    path: [
      "./src/**/*.{ts,tsx,js,jsx,mjs,cjs,astro,vue,svelte}",
      "./app/**/*.{ts,tsx,js,jsx,mjs,cjs,astro,vue,svelte}",
      "./sanity/**/*.{ts,tsx,js,jsx,mjs,cjs}",
    ], // glob patterns for the files that contain your queries. Can also be a single string
    schema: "./schema.json", // path to your schema file, generated with 'sanity schema extract' command
    generates: "./sanity.types.ts", // path to the output file for generated type definitions
    overloadClientMethods: true, // set to false to disable automatic overloading the sanity client
  },
})

The example above is shown with the default values. Note that the path can be defined as either an array of strings or a single string.

Deprecated typegen config file

Watch mode

Both sanity schema extract and sanity typegen generate feature a --watch flag (from v5.8.0) to watch for changes in your files and generate the schema and type files as you change things in your project.

The schema extraction watch mode watches for changes in your Studio and generates the schema.json file.

The type generation watches the configured schema path and files you've configured your type generation to look for.

$ cd ~/frontend
$ sanity typegen generate --watch
✔ Config loaded from sanity.cli.ts
✔ Schema loaded from ./schema.json
✔ Successfully generated types to ./sanity.types.ts in 328ms
  └─ 0 queries and 18 schema types
  └─ found queries in 0 files after evaluating 1 file
  └─ formatted the generated code with prettier
[5:42:03 PM] change: src/queries.ts
✔ Schema loaded from ./schema.json
✔ Successfully generated types to ./sanity.types.ts in 414ms
  └─ 1 query and 18 schema types
  └─ found queries in 1 file after evaluating 1 file
  └─ formatted the generated code with prettier

Generated files and version control

Commit schema.json and sanity.types.ts to your repository. Both files are generated, but tracking them means every developer, CI job, and type check starts from the same types without running the CLI first.

Sanity's starter templates commit these files. They also regenerate them before the dev and build scripts run, so the committed types stay in step with the schema. The --force flag lets sanity schema extract overwrite the committed schema.json without prompting. Without it, the command asks for confirmation locally and exits with an error in CI:

{
  "scripts": {
    "typegen": "sanity schema extract --force && sanity typegen generate",
    "predev": "npm run typegen",
    "prebuild": "npm run typegen"
  }
}

Teams working in the same schema often hit merge conflicts in sanity.types.ts. Because the file is regenerated in full on every run, you never have to resolve one by hand. Take either side of the conflict, then regenerate:

$ npx sanity schema extract --force
$ npx sanity typegen generate

Ignoring the generated files

Example: embedded Studio

A common pattern is to embed Sanity Studio in another application, keeping everything in a single repository. You can find several example repositories that follow this convention in the templates section of the Sanity Exchange. Because your studio and application share a single project root, this is the most straightforward setup. The only adjustment you might need is to set typegen.path to where your queries live. If your queries are in ./src, ./app, or ./sanity, the default settings already cover them.

Example: monorepo

Another common way of structuring a Sanity-powered project is to create a "monorepo" within which your Studio and your consuming application live separately side by side, possibly in sub-repositories of their own. Depending on the needs and preferences of your project you could use the configuration options of each CLI command to output the generated files into your consuming application, or you could keep the generated files in the Studio folder and put it on the application to find them by traversing the monorepo.

# Use the --path flag to output schema.json elsewhere

npx sanity schema extract --path ../../my-cool-app/sanity-schemas.json
import { defineCliConfig } from 'sanity/cli';

export default defineCliConfig({
  // ...rest of config
  typegen: {
    path: "../../my-cool-app/src/**/*.{ts,tsx,js,jsx}", // glob pattern to your typescript files
    schema: "../../my-cool-app/sanity-schemas.json", // path to your schema file, generated with 'sanity schema extract' command
    generates: "../../my-cool-app/sanity.types.ts" // path to the output file for generated type definitions
  },
})

Example: separate repos

Use this recipe when your consumer (frontend or app) lives in a different repository from your Studio. The consumer repo runs TypeGen against a schema.json file extracted from the Studio.

In the Studio repo: extract the schema

Run sanity schema extract and write the output to a path your frontend can read:

npx sanity schema extract --path ../my-frontend/schema.json --force

For automated workflows, run this command in CI on every Studio deploy, or publish schema.json through a shared internal package.

In the frontend repo: install and configure

Install the required packages. The full sanity package is required for the CLI commands. Add it to devDependencies so it stays out of your runtime bundle:

npm install --save-dev sanity

Create sanity.cli.ts at the root of your frontend repo and configure the typegen block:

import {defineCliConfig} from 'sanity/cli'

export default defineCliConfig({
  typegen: {
    path: './src/**/*.{ts,tsx,js,jsx}',
    schema: './schema.json',
    generates: './sanity.types.ts',
  },
})

Generate types. Use watch mode in development to regenerate types as your queries change:

# Development: regenerate on save
npx sanity typegen generate --watch

# CI: run once
npx sanity typegen generate

Was this page helpful?

Sanity logosanity.ioAll Systems Operational© Sanity 2026
Change site theme