App SDK and TypeGen

Generate per-dataset types with Sanity TypeGen so App SDK hooks infer document, query, and projection types.

Sanity TypeGen generates TypeScript types from your schemas and GROQ queries. With typegen.resources, it writes one types file per dataset and registers each file's types under that dataset's projectId.dataset key. App SDK hooks then infer document, field, query, and projection types from the project and dataset a handle points at, so you don't pass result types to hooks yourself.

If your app uses the experimental TypeGen packages (versions ending in -typegen-experimental.0, or a sanity-typegen.json file with unstable_schemas), see Migrate from experimental TypeGen in the App SDK.

Prerequisites

Package

Version

@sanity/cli

8.16.0 or later. It includes TypeGen (@sanity/codegen 8.2.0).

@sanity/sdk-react and @sanity/sdk

3.9.1 or later

groq

6.12.0 or later, if your code imports defineQuery

If your app doesn't depend on groq yet, install it before you use defineQuery:

npm install groq

The sanity package depends on @sanity/cli, but your lockfile can still resolve an older version. Check which version runs:

npx sanity --version

If it reports a version earlier than 8.16.0, add the CLI as a dev dependency so the sanity command uses a version that supports typegen.resources:

npm install --save-dev @sanity/cli@^8.16.0

Set up TypeGen for your datasets

Get each dataset's schema

TypeGen needs the schema for every dataset it generates types for. Extract the schema in the studio that defines it, and copy the file into your app:

npx sanity schema extract --path schema.production.json

If your studio has more than one workspace, add --workspace <name>. To make fields with a required validation rule non-optional in the generated types, add --enforce-required-fields. To replace an existing schema file, add --force.

You can also leave out the schema file. The CLI then fetches the schema bound to the dataset, which requires sanity login (or SANITY_AUTH_TOKEN in CI). Most datasets don't have a bound schema yet, and generation then stops with a "No schema is bound to dataset" error. Use an extracted file in that case.

Declare your datasets in one module

Put the datasets your app reads in a module that both the app and the CLI configuration import:

import type {DocumentResource} from '@sanity/sdk-react'

export const resources = {
  production: {projectId: 'abc123', dataset: 'production'},
  staging: {projectId: 'abc123', dataset: 'staging'},
} as const satisfies Record<string, DocumentResource>

as const keeps the project ID and dataset as literal types, which type inference depends on. The CLI loads this module in Node.js, so it must not use browser globals such as window. Replace abc123 with your project ID, here and wherever it appears in a DocumentHandle type.

Configure TypeGen

List each dataset under typegen.resources in sanity.cli.ts:

import {defineCliConfig} from 'sanity/cli'

import {resources} from './src/resources'

export default defineCliConfig({
  app: {organizationId: 'YOUR_ORGANIZATION_ID', entry: './src/App.tsx'},
  typegen: {
    resources: [
      {
        ...resources.production,
        schema: './schema.production.json',
        generates: './src/sanity.types.production.ts',
      },
      {
        ...resources.staging,
        schema: './schema.staging.json',
        generates: './src/sanity.types.staging.ts',
      },
    ],
  },
})

Replace YOUR_ORGANIZATION_ID with your organization ID. Each entry in typegen.resources accepts these properties:

  • The dataset to generate types for. Each project ID and dataset pair can appear once.

  • generates

    string

    The file to write. Each resource needs its own file.

  • schema

    string

    A schema file from sanity schema extract. Leave it out to fetch the schema bound to the dataset.

  • path

    string | string[]

    Glob pattern, or patterns, for the files to scan for queries and projections. Defaults to typegen.path, which defaults to files under src, app, and sanity.

  • For a fetched schema, makes fields with a required validation rule non-optional. Defaults to false. It has no effect with a schema file: pass --enforce-required-fields to sanity schema extract instead.

When resources is set, TypeGen ignores the top-level typegen.schema and typegen.generates properties, and the CLI warns if they're set.

Generate types

Run TypeGen from your app directory:

npx sanity typegen generate

Run it again after you change a schema, query, or projection. To keep types current, run it before type checking, for example from your typecheck script.

Watch mode and generation during sanity dev don't support resources yet. sanity typegen generate --watch exits with the error "Watch mode does not support typegen.resources yet", and sanity dev with typegen.enabled skips generation and logs a warning.

Work with the generated files

Each generated file exports the dataset's schema types and a result type for each query and projection that TypeGen found. It also registers these types under the dataset's projectId.dataset key, which is how hooks find them. Don't edit these files.

  • Keep the files inside your TypeScript program. With an include of src in tsconfig.json, write them under src. Hooks can't see registrations in files outside the program.
  • Import a type from the file for the dataset you mean. Two datasets can both export a Book type with different fields, for example import type {Book} from './sanity.types.staging'.
  • Commit the generated files, or generate them in CI before type checking. With schema files, generation needs no network access or login. Fetching a bound schema in CI needs SANITY_AUTH_TOKEN.

Type documents and fields

Create document handles from your declared datasets. createDocumentHandle keeps the project ID and dataset as literal types, so useDocument returns the generated document type for that dataset:

import {createDocumentHandle, useDocument} from '@sanity/sdk-react'

import {resources} from './resources'

export const book = createDocumentHandle({
  ...resources.production,
  documentId: 'book-1',
  documentType: 'book',
})

export function BookTitle() {
  // `Book` from sanity.types.production.ts, or null
  const {data: doc} = useDocument(book)
  // string | undefined
  const {data: title} = useDocument({...book, path: 'title'})
  return <h1>{title ?? doc?._id}</h1>
}

Type GROQ queries

Define queries with defineQuery from groq, and assign each one to a variable. TypeGen generates a result type for the exact query text, for every dataset whose path includes the file:

import {useQuery} from '@sanity/sdk-react'
import {defineQuery} from 'groq'

import {resources} from './resources'

export const bookTitlesQuery = defineQuery('*[_type == "book"]{_id, title}')

export function BookTitles() {
  // {_id: string; title: number | null}[] in staging
  const {data} = useQuery({...resources.staging, query: bookTitlesQuery})
  return (
    <ul>
      {data.map((book) => (
        <li key={book._id}>{book.title}</li>
      ))}
    </ul>
  )
}

The same query text resolves to each dataset's own result type. TypeGen can only read static query text: string literals, and template literals and constants that resolve to one. Query variable names must be unique across the scanned files.

Type projections

Define projections with defineProjection from @sanity/sdk-react or @sanity/sdk, and assign each one to a variable. Pass the document type as the first argument, so TypeGen evaluates the projection for that type:

import {defineProjection, useDocumentProjection, type DocumentHandle} from '@sanity/sdk-react'

export const bookCardProjection = defineProjection('book', '{title, "authorName": author->name}')

export function BookCard({doc}: {doc: DocumentHandle<'book', 'production', 'abc123'>}) {
  // {title: string | null; authorName: string | null}
  const {data} = useDocumentProjection({...doc, projection: bookCardProjection})
  return (
    <span>
      {data.title} by {data.authorName ?? 'Unknown'}
    </span>
  )
}

defineProjection returns the projection string unchanged. The document type only tells TypeGen what to generate. Without one, as in defineProjection('{title}'), TypeGen evaluates the projection for every document type in the schema, and a handle whose document type is only known as string gets the union of those results.

Understand which dataset's types a hook uses

Hooks look up types by the project ID and dataset as TypeScript sees them on the handle or options:

Project ID and dataset in the type

Result

Literal: a handle from createDocumentHandle with a declared resource, or a prop typed DocumentHandle<'book', 'production', 'abc123'>

That dataset's types.

Unknown: a prop typed DocumentHandle<'book'>, a handle without projectId and dataset that uses the provider's resource, or a dataset chosen at runtime

The union of the matching types from every generated dataset. With one dataset, that dataset's types.

A dataset with no generated file

A generic document with unknown fields.

DocumentHandle takes the document type, then the dataset, then the project ID. A prop typed with only the document type accepts a handle from any dataset, so its fields have the types from every dataset:

import {useDocument, type DocumentHandle} from '@sanity/sdk-react'

export function AnyBookTitle({doc}: {doc: DocumentHandle<'book'>}) {
  // string | number | undefined: the title from either dataset
  const {data: title} = useDocument({...doc, path: 'title'})
  return <h1>{title}</h1>
}

A query or projection that TypeGen didn't generate for the dataset, such as a query string built at runtime, resolves to never. Pass the result type yourself in that case. An explicit type parameter always takes precedence:

const {data} = useQuery<{count: number}>({...resources.staging, query: '{"count": count(*)}'})

Media Library and Canvas resources have no generated types. Pass result types for them explicitly.

Use TypeGen with a single schema

TypeGen still supports a single schema with the top-level typegen.schema and typegen.generates properties. This output doesn't register types for a dataset, so hooks don't infer types from it. Import the generated types and pass them as type parameters instead.

Set overloadClientMethods: false in typegen unless your app depends on @sanity/client directly. Otherwise the generated file augments @sanity/client, and TypeScript reports "Invalid module name in augmentation" when the app can't resolve that package, as with pnpm.

import {defineCliConfig} from 'sanity/cli'
export default defineCliConfig({
  app: {organizationId: 'YOUR_ORGANIZATION_ID', entry: './src/App.tsx'},
  typegen: {schema: './schema.json', generates: './src/sanity.types.ts', overloadClientMethods: false},
})

After you run npx sanity typegen generate, import the generated types from sanity.types.ts and pass them to the hooks:

import {useDocument, useQuery, type DocumentHandle} from '@sanity/sdk-react'
import {defineQuery} from 'groq'

import type {AllBooksResult, Book} from './sanity.types'

export const allBooks = defineQuery('*[_type == "book"]{_id, title}')

export function BookTitle({doc}: {doc: DocumentHandle<'book'>}) {
  const {data: book} = useDocument<Book>(doc)
  return <h1>{book?.title ?? 'Untitled'}</h1>
}

export function BookList() {
  const {data} = useQuery<AllBooksResult>({query: allBooks})
  return (
    <ul>
      {data.map((book) => (
        <li key={book._id}>{book.title}</li>
      ))}
    </ul>
  )
}

Troubleshooting

Symptom

Cause and fix

A hook's result is never

TypeGen didn't generate that query or projection for the dataset. Check that the call is assigned to a variable, that the file is inside the resource's path, that the text is static, and that you ran sanity typegen generate after changing it.

A hook's result is a generic document

The handle's dataset has no generated file, or the generated file is outside your TypeScript program.

A result is a union of two datasets' types

The handle's dataset isn't a literal type. Type the prop with its dataset and project ID, or create the handle from a declared resource.

"No schema is bound to dataset"

The dataset has no bound schema. Extract the schema with sanity schema extract and set schema for that resource.

"Duplicate query name found"

Two scanned files define a query with the same variable name. Rename one.

"Schema file not found: …/schema.json" with resources set

The CLI that runs is older than 8.16.0, so it ignores resources. Install @sanity/cli 8.16.0 or later as a dev dependency.

Typed client.fetch results are gone

With resources, overloadClientMethods defaults to false. SDK hooks don't use these overloads. With a single resource, set overloadClientMethods: true to get them back. With more than one, the generated files would conflict.

Was this page helpful?

sanity.ioAll Systems Operational© Sanity 2026
Change site theme