App SDK and TypeGen
Generate per-dataset types with Sanity TypeGen so App SDK hooks infer document, query, and projection types.
TypeGen support for the App SDK is in beta. typegen.resources, the SDK's type resolvers, and defineProjection with a document type can change in minor releases. Fetching a dataset's bound schema uses an experimental API.
This page is for using TypeGen with the App SDK. See the TypeGen article to generate types for your studio and front-end applications.
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 |
|---|---|
| 8.16.0 or later. It includes TypeGen ( |
| 3.9.1 or later |
| 6.12.0 or later, if your code imports |
If your app doesn't depend on groq yet, install it before you use defineQuery:
npm install groqpnpm add groqyarn add groqbun add groqThe sanity package depends on @sanity/cli, but your lockfile can still resolve an older version. Check which version runs:
npx sanity --versionpnpm dlx sanity --versionyarn dlx sanity --versionbunx sanity --versionIf 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.0pnpm add --save-dev @sanity/cli@^8.16.0yarn add --dev @sanity/cli@^8.16.0bun add --dev @sanity/cli@^8.16.0Set 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.jsonpnpm dlx sanity schema extract --path schema.production.jsonyarn dlx sanity schema extract --path schema.production.jsonbunx sanity schema extract --path schema.production.jsonIf 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:
projectId, dataset
string
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 undersrc,app, andsanity.enforceRequiredFields
boolean
For a fetched schema, makes fields with a required validation rule non-optional. Defaults to
false. It has no effect with aschemafile: pass--enforce-required-fieldstosanity schema extractinstead.
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 generatepnpm dlx sanity typegen generateyarn dlx sanity typegen generatebunx sanity typegen generateRun 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
includeofsrcintsconfig.json, write them undersrc. 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
Booktype with different fields, for exampleimport 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 | That dataset's types. |
Unknown: a prop typed | 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 | 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 |
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 |
"Duplicate query name found" | Two scanned files define a query with the same variable name. Rename one. |
"Schema file not found: …/schema.json" with | The CLI that runs is older than 8.16.0, so it ignores |
Typed | With |
Was this page helpful?
Feedback submitted, thanks for helping us improve the docs. Have you tried these alternatives?