Sanity logosanity.ioAll Systems Operational© Sanity 2026
Change Site Theme
Sanity logo

Documentation

    • Overview
    • Platform introduction
    • Next.js quickstart
    • Nuxt.js quickstart
    • Astro quickstart
    • React Router quickstart
    • Studio quickstart
    • Build with AI
    • Content Lake
    • Functions
    • APIs and SDKs
    • Agent Actions
    • Visual Editing
    • Blueprints
    • Platform management
    • Dashboard
    • Studio
    • Canvas
    • Media Library
    • App SDK
    • Content Agent
    • HTTP API
    • CLI
    • Libraries
    • Specifications
    • Changelog
    • User guides
    • Developer guides
    • Courses and certifications
    • Join the community
    • Templates
App SDK
Overview

  • Quickstart
  • Introduction
  • Learn App SDK Course
  • Setup and development

    Installation
    Configuration
    Deployment

  • Concepts

    Document Handles
    React Hooks
    Suspense
    Authentication
    App SDK best practices

  • Guides

    Fetching and handling content
    Editing documents

  • Style your app

    Sanity UI
    Tailwind CSS

  • TypeScript

    TypeGen (Experimental)

  • Reference

    @sanity/sdk-react
    SDK Explorer

On this page

Previous

Tailwind CSS

Was this page helpful?

On this page

  • Setup
  • Extract schemas
  • Install (experimental) packages
  • Configure TypeGen (optional)
  • Generate types
  • Use the generated types
  • Handles and literal types
  • Document projections
  • GROQ queries
  • Document lists
  • Specific document types
  • Workflow considerations
  • Regeneration
  • TypeGen is additive
  • JavaScript projects
App SDKLast updated May 15, 2025

App SDK and TypeGen

Learn how to use Sanity TypeGen with the App SDK for increased type safety and improved developer experience.

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 is a tool that generates TypeScript types directly from your Sanity schemas and GROQ queries. When used with the Sanity App SDK, it provides strong type safety and autocompletion suggestions for your documents, query results, and projections.

In this guide, we’ll walk through setting up and using TypeGen within your SDK app.

Experimental feature

TypeGen support in the App SDK is currently in its early stages. We’re actively working on improving this integration and the developer experience around it. For now, some parts of this process may be suboptimal, but we invite the adventurous among you to follow along!

Setup

Using Typegen involves two main steps: extracting your schema(s) and then generating the types. Both commands are available via the CLI.

Extract schemas

First, you need to extract your Sanity schema(s) into a JSON format that Typegen can understand. Currently, this step relies on the full sanity package, typically used within your Sanity Studio project, as Typegen needs access to the complete schema definition to generate accurate types.

Schema extraction is performed within your Studio setup to generate the schema.json file. Once created, this file can be used independently by other tools or parts of your workflow.

We recognize that requiring the Studio environment solely for this generation step isn't ideal, and we're actively working on improving this workflow in future App SDK updates to make the process more self-contained.

Use the sanity schema extract command within your Studio project or a project that has the sanity package installed:

npx sanity schema extract --workspace <workspace-name> --output-path <path/to/schema.json>
pnpm dlx sanity schema extract --workspace <workspace-name> --output-path <path/to/schema.json>
yarn dlx sanity schema extract --workspace <workspace-name> --output-path <path/to/schema.json>
bunx sanity schema extract --workspace <workspace-name> --output-path <path/to/schema.json>

This schema.json file can be copied to (or the --output-path can be set directly to) your Sanity app's repository. Your application itself does not need the full sanity package as a dependency to use the generated types; it only needs the schema.json file for the typegen generate step.

If your Studio project defines multiple workspaces or you need types for different schemas (e.g., for different datasets), run the extract command for each one, outputting to separate JSON files. For example, you could configure you Studio’s package.json as follows:

{
  "scripts": {
    "schema:extract:test": "sanity schema extract --workspace test --output-path ../my-frontend-app/schema-test.json",
    "schema:extract:prod": "sanity schema extract --workspace production --output-path ../my-frontend-app/schema-prod.json",
    "schema:extract": "npm run schema:extract:test && npm run schema:extract:prod"
  }
}

We plan to improve this schema extraction process as the SDK matures to potentially reduce the dependencies and improve overall developer experience.

Install (experimental) packages

To use the Typegen features described in this guide, your SDK app needs specific experimental versions of @sanity/cli and groq installed. Install these packages from within your SDK app directory:

npm install groq@typegen-experimental-2025-04-23
npm install --save-dev @sanity/cli@typegen-experimental-2025-04-23
pnpm add groq@typegen-experimental-2025-04-23
pnpm add --save-dev @sanity/cli@typegen-experimental-2025-04-23
yarn add groq@typegen-experimental-2025-04-23
yarn add --dev @sanity/cli@typegen-experimental-2025-04-23
bun add groq@typegen-experimental-2025-04-23
bun add --dev @sanity/cli@typegen-experimental-2025-04-23

Package names and installation

These are experimental pre-release versions. The package names and installation process may change as these features stabilize.

Configure TypeGen (optional)

For the most common use case – a single Sanity schema for your project – no configuration file is needed. However, you'll need to create a TypeGen configuration file for more complex use cases, such as:

  • Using multiple schemas (e.g., from different workspaces or for different datasets).
  • Needing to explicitly map a single schema to a specific schemaId for accurate schema scoping (instead of using the default 'default').
  • Using a different name or location for your schema file(s).
  • Specifying a custom output path for the generated types file.

If you need this level of configuration, create a TypeGen configuration file (sanity-typegen.json ) at the root of your SDK app and use the unstable_schemas array:

// sanity-typegen.json
{
  "unstable_schemas": [
    {
      // Path to the schema
      "schemaPath": "./schemas/products-schema.json",
      // The schema ID, formatted as `projectId.datasetName`
      "schemaId": "your-project-id.products"
    },
    {
      "schemaPath": "./schemas/authors-schema.json",
      "schemaId": "your-project-id.authors"
    }
    // Add more schema objects if needed
  ],
  "overloadClientMethods": false // client methods are not needed for the App SDK
  // Optional: Specify output path for generated types
  // "outputPath": "./src/generated/sanity-types.ts"
}

Objects in the unstable_schemas array each consist of the following properties:

  • schemaPath: The path (relative to the project root) to the corresponding extracted schema JSON file.
  • schemaId: A string combining your projectId and dataset (e.g., "your-project-id.your-dataset-name"). This is used to map the schema to the correct project and dataset context for type generation, as the extracted schema.json doesn't contain this information itself.

The optional outputPath property specifies where to write the generated sanity.types.ts file. It defaults to the project root.

By default, TypeGen works seamlessly for the common single-schema setup without extra configuration. Use sanity-typegen.json only when your needs require more explicit control.

Generate types

With the necessary packages installed and your schema(s) extracted (and optionally configured in sanity-typegen.json), you can run the sanity typegen generate command from within your SDK app directory:

# use `@sanity/cli` package directly for now
./node_modules/@sanity/cli/bin/sanity typegen generate

This command reads your configuration (either sanity-typegen.json or the default schema.json), processes the specified schemas, and generates a sanity.types.ts file, which contains your types. It's recommended to add this command to your SDK app’s package.json scripts. For example:

{
  "scripts": {
    "typegen": "./node_modules/@sanity/cli/bin/sanity typegen generate"
  }
}

Congratulations! You’ve now generated types for your schema documents, projections, and query results. With your sanity.types.ts file in place, the App SDK hooks will automatically pick up these types.

Next, we’ll cover how to make use of these generated types in your SDK app.

Use the generated types

TypeGen generates interfaces for each document type defined in your schemas. For projects using multiple schemas/datasets defined in sanity-typegen.json, it utilizes a helper type SchemaOrigin (imported from groq) to brand the types.

This allows TypeScript to narrow down the possible document types based on the dataset context provided via a DocumentHandle. See the code below for an example of this:

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

// Assuming 'book' is only in 'test' dataset, 'dog' only in 'production'
const testHandle = createDocumentHandle({
  projectId: 'your-project-id',
  dataset: 'test',
  documentId: 'some-id',
  documentType: 'book', // Type narrowed to 'book'
})

const prodHandle = createDocumentHandle({
  projectId: 'your-project-id',
  dataset: 'production',
  documentId: 'another-id',
  documentType: 'dog', // Type narrowed to 'dog'
})

function MyComponent() {
  const {data: bookData} = useDocument(testHandle)
  // bookData is correctly typed as Book

  const {data: dogData} = useDocument(prodHandle)
  // dogData is correctly typed as Dog

  // ...
}

Handles and literal types

For TypeGen to correctly infer types in hooks like useDocument, it needs to know the specific literal type of the documentType (e.g., 'book' instead of just string).

The App SDK provides helper functions (like createDocumentHandle and createDatasetHandle) that help capture these literal types:

import {createDocumentHandle} from '@sanity/sdk'

// Using the helper ensures handle.documentType is typed as 'book'
const handle = createDocumentHandle({
  documentId: '123',
  documentType: 'book',
  dataset: 'production',
  projectId: 'abc',
})

Alternatively, if you prefer defining handles as plain objects, use as const to ensure the documentType has the literal type of 'book':

const handle = {
  documentId: '123',
  documentType: 'book',
  dataset: 'production',
  projectId: 'abc',
} as const // 'as const' ensures documentType is 'book', not string

We recommend that you use createDocumentHandle (or other create*Handle helpers) when using Typegen for cleaner code.

Document projections

To get types for GROQ projections used with useDocumentProjection and TypeGen, you must define them using the defineProjection helper from groq.

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

// Typegen derives the type name (AuthorSummaryProjectionResult) from the variable name
export const authorSummary = defineProjection(`{
  "name": name,
  "favoriteBookTitles": favoriteBooks[]->title,
}`)

function AuthorDetails({doc}: {doc: DocumentHandle<'author'>}) {
  // The type of `data` is inferred from `authorProjection`
  const {data} = useDocumentProjection({
    ...doc, // Spread the handle containing documentId, type, etc.
    projection: authorProjection,
  })

  // data is typed as AuthorSummaryProjectionResult
  // Autocompletion works for data.name and data.favoriteBookTitles
  return <div>{data.name}</div>
}

There are few important things to note here:

  • In the example above, the generated type (e.g., AuthorSummaryProjectionResult) includes a ProjectionBase brand, allowing unions of projection results if a projection applies to multiple document types.
  • TypeGen intelligently removes types from the projection result if all fields in the projection evaluate to null for a given document type.
  • When using Typegen, you cannot pass raw projection strings to useDocumentProjection and get type inference; you must use defineProjection.

GROQ queries

Similarly to useProjection, when using the useQuery hook, you must define your GROQ queries using defineQuery from the groq package to get type inference:

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

// Typegen derives the type name (AllBooksQuery) from the variable name
export const allBooksQuery = defineQuery('*[_type == "book"]{ _id, title }')

function BookList() {
  // Type of `data` is inferred from `allBooksQuery`
  const {data} = useQuery({query: allBooksQuery})

  // data is typed as Array<{_id: string, title: string}> (or similar)
  return (
    <ul>
      {data.map((book) => (
        <li key={book._id}>{book.title}</li>
      ))}
    </ul>
  )
}

Note that useQuery accepts options as a single object, allowing you to spread handles easily. For example:

const handle = createDatasetHandle({dataset: 'test', projectId: 'abc'})
const {data} = useQuery({...handle, query: allBooksQuery})

Document lists

The App SDK’s document list hooks, useDocuments and usePaginatedDocuments, benefit from TypeGen through dataset scoping (as shown earlier). You can use the documentType option to specify the document type(s) you are querying:

import {usePaginatedDocuments, createDatasetHandle} from '@sanity/sdk-react'
import {DocumentPreview} from './your-document-preview'

const testDataset = createDatasetHandle({dataset: 'test', projectId: 'abc'})

function MixedList() {
  // Specify the types being queried
  const {data} = usePaginatedDocuments({
    ...testDataset,
    documentType: ['author', 'book'], // Pass string or array of strings
  })

  // `data` is an array of DocumentHandles, correctly scoped.
  // If used with `useDocument` (and other hooks) later, types will be scoped
  // appropriately (e.g. Author | Book).
  return (
    <ul>
      {data.map((doc) => (
        <Suspense key={doc.documentId} fallback={<li>Loading...</li>}>
          <DocumentPreview doc={doc} />
        </Suspense>
      ))}
    </ul>
  )
}

Specific document types

When you know the specific document type you're dealing with, you can make your TypeScript code even more precise using the methods described below.

Parameterizing DocumentHandles

DocumentHandle is a generic type that accept type parameters. You can provide a specific document type literal (like 'book') as a type argument. This is useful for typing props or variables that should only reference a handle for a specific document type:

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

// This function expects a handle that *must* reference a 'book' document
function BookComponent({doc}: {doc: DocumentHandle<'book'>}) {
  // Thanks to DocumentHandle<'book'>, TypeScript knows the context
  const {data} = useDocument(doc)
  // `data` will be typed as the generated `Book` interface
  // ...
}

This works because the full definition of DocumentHandle includes generic type parameters (TDocumentType, TDataset, TProjectId) that default to string but can be made more specific.

Using SanityDocument for document data

If you need the type for the actual document data itself (not just the handle), the groq package exports the SanityDocument<TDocumentType> helper type. Pass the document type literal to get the corresponding generated interface for the document content:

import {type SanityDocument} from 'groq'

type BookData = SanityDocument<'book'>
// BookData is now equivalent to the generated Book interface (e.g., { _id: string; title: string; ... })

// This function expects the fully typed book data
function processBook(book: BookData) {
  console.log(book.title) // Autocomplete works!
}

In summary:

  • Use DocumentHandle<'yourType'> to constrain a document handle to documents of a specific type.
  • Use SanityDocument<'yourType'> to type the actual data structure of a document of a specific type.

Workflow considerations

Regeneration

You'll need to re-run npm run typegen whenever you:

  • Change your Sanity schemas.
  • Add or modify queries/projections defined with defineQuery or defineProjection.
  • Consider integrating this into your dev script or a file watcher.

TypeGen is additive

TypeGen is designed to enhance the App SDK experience. If you don't use it, the App SDK hooks will still work, but data types will often default to any or unknown, losing the benefits of TypeScript. Adopting TypeGen later should be a non-breaking change that simply adds type safety.

JavaScript projects

Even if your project doesn't use TypeScript, you can still leverage TypeGen to enhance your JavaScript development experience.

By following the steps in this guide – extracting your schema, installing the necessary packages, using helpers like createDocumentHandle, defineProjection, and defineQuery, and running npm run typegen – you create a sanity.types.ts file.

While your JavaScript code won't undergo compile-time type checking, modern code editors (like VS Code) that use the TypeScript language service can read this generated file.

This often results in significantly better autocompletion within your JavaScript files when interacting with App SDK hooks and data. Remember, however, that using defineProjection and defineQuery is still required for TypeGen to generate types for those specific artifacts.

npx sanity schema extract --workspace <workspace-name> --output-path <path/to/schema.json>
pnpm dlx sanity schema extract --workspace <workspace-name> --output-path <path/to/schema.json>
yarn dlx sanity schema extract --workspace <workspace-name> --output-path <path/to/schema.json>
bunx sanity schema extract --workspace <workspace-name> --output-path <path/to/schema.json>
npx sanity schema extract --workspace <workspace-name> --output-path <path/to/schema.json>
pnpm dlx sanity schema extract --workspace <workspace-name> --output-path <path/to/schema.json>
yarn dlx sanity schema extract --workspace <workspace-name> --output-path <path/to/schema.json>
bunx sanity schema extract --workspace <workspace-name> --output-path <path/to/schema.json>
{
  "scripts": {
    "schema:extract:test": "sanity schema extract --workspace test --output-path ../my-frontend-app/schema-test.json",
    "schema:extract:prod": "sanity schema extract --workspace production --output-path ../my-frontend-app/schema-prod.json",
    "schema:extract": "npm run schema:extract:test && npm run schema:extract:prod"
  }
}
npm install groq@typegen-experimental-2025-04-23
npm install --save-dev @sanity/cli@typegen-experimental-2025-04-23
pnpm add groq@typegen-experimental-2025-04-23
pnpm add --save-dev @sanity/cli@typegen-experimental-2025-04-23
yarn add groq@typegen-experimental-2025-04-23
yarn add --dev @sanity/cli@typegen-experimental-2025-04-23
bun add groq@typegen-experimental-2025-04-23
bun add --dev @sanity/cli@typegen-experimental-2025-04-23
npm install groq@typegen-experimental-2025-04-23
npm install --save-dev @sanity/cli@typegen-experimental-2025-04-23
pnpm add groq@typegen-experimental-2025-04-23
pnpm add --save-dev @sanity/cli@typegen-experimental-2025-04-23
yarn add groq@typegen-experimental-2025-04-23
yarn add --dev @sanity/cli@typegen-experimental-2025-04-23
bun add groq@typegen-experimental-2025-04-23
bun add --dev @sanity/cli@typegen-experimental-2025-04-23
// sanity-typegen.json
{
  "unstable_schemas": [
    {
      // Path to the schema
      "schemaPath": "./schemas/products-schema.json",
      // The schema ID, formatted as `projectId.datasetName`
      "schemaId": "your-project-id.products"
    },
    {
      "schemaPath": "./schemas/authors-schema.json",
      "schemaId": "your-project-id.authors"
    }
    // Add more schema objects if needed
  ],
  "overloadClientMethods": false // client methods are not needed for the App SDK
  // Optional: Specify output path for generated types
  // "outputPath": "./src/generated/sanity-types.ts"
}
# use `@sanity/cli` package directly for now
./node_modules/@sanity/cli/bin/sanity typegen generate
{
  "scripts": {
    "typegen": "./node_modules/@sanity/cli/bin/sanity typegen generate"
  }
}
import {useDocument, createDocumentHandle} from '@sanity/sdk-react'

// Assuming 'book' is only in 'test' dataset, 'dog' only in 'production'
const testHandle = createDocumentHandle({
  projectId: 'your-project-id',
  dataset: 'test',
  documentId: 'some-id',
  documentType: 'book', // Type narrowed to 'book'
})

const prodHandle = createDocumentHandle({
  projectId: 'your-project-id',
  dataset: 'production',
  documentId: 'another-id',
  documentType: 'dog', // Type narrowed to 'dog'
})

function MyComponent() {
  const {data: bookData} = useDocument(testHandle)
  // bookData is correctly typed as Book

  const {data: dogData} = useDocument(prodHandle)
  // dogData is correctly typed as Dog

  // ...
}
import {createDocumentHandle} from '@sanity/sdk'

// Using the helper ensures handle.documentType is typed as 'book'
const handle = createDocumentHandle({
  documentId: '123',
  documentType: 'book',
  dataset: 'production',
  projectId: 'abc',
})
const handle = {
  documentId: '123',
  documentType: 'book',
  dataset: 'production',
  projectId: 'abc',
} as const // 'as const' ensures documentType is 'book', not string
import {defineProjection} from 'groq'
import {useDocumentProjection, type DocumentHandle} from '@sanity/sdk-react'

// Typegen derives the type name (AuthorSummaryProjectionResult) from the variable name
export const authorSummary = defineProjection(`{
  "name": name,
  "favoriteBookTitles": favoriteBooks[]->title,
}`)

function AuthorDetails({doc}: {doc: DocumentHandle<'author'>}) {
  // The type of `data` is inferred from `authorProjection`
  const {data} = useDocumentProjection({
    ...doc, // Spread the handle containing documentId, type, etc.
    projection: authorProjection,
  })

  // data is typed as AuthorSummaryProjectionResult
  // Autocompletion works for data.name and data.favoriteBookTitles
  return <div>{data.name}</div>
}
import {defineQuery} from 'groq'
import {useQuery} from '@sanity/sdk-react'

// Typegen derives the type name (AllBooksQuery) from the variable name
export const allBooksQuery = defineQuery('*[_type == "book"]{ _id, title }')

function BookList() {
  // Type of `data` is inferred from `allBooksQuery`
  const {data} = useQuery({query: allBooksQuery})

  // data is typed as Array<{_id: string, title: string}> (or similar)
  return (
    <ul>
      {data.map((book) => (
        <li key={book._id}>{book.title}</li>
      ))}
    </ul>
  )
}
const handle = createDatasetHandle({dataset: 'test', projectId: 'abc'})
const {data} = useQuery({...handle, query: allBooksQuery})
import {usePaginatedDocuments, createDatasetHandle} from '@sanity/sdk-react'
import {DocumentPreview} from './your-document-preview'

const testDataset = createDatasetHandle({dataset: 'test', projectId: 'abc'})

function MixedList() {
  // Specify the types being queried
  const {data} = usePaginatedDocuments({
    ...testDataset,
    documentType: ['author', 'book'], // Pass string or array of strings
  })

  // `data` is an array of DocumentHandles, correctly scoped.
  // If used with `useDocument` (and other hooks) later, types will be scoped
  // appropriately (e.g. Author | Book).
  return (
    <ul>
      {data.map((doc) => (
        <Suspense key={doc.documentId} fallback={<li>Loading...</li>}>
          <DocumentPreview doc={doc} />
        </Suspense>
      ))}
    </ul>
  )
}
import {type DocumentHandle} from '@sanity/sdk-react'

// This function expects a handle that *must* reference a 'book' document
function BookComponent({doc}: {doc: DocumentHandle<'book'>}) {
  // Thanks to DocumentHandle<'book'>, TypeScript knows the context
  const {data} = useDocument(doc)
  // `data` will be typed as the generated `Book` interface
  // ...
}
import {type SanityDocument} from 'groq'

type BookData = SanityDocument<'book'>
// BookData is now equivalent to the generated Book interface (e.g., { _id: string; title: string; ... })

// This function expects the fully typed book data
function processBook(book: BookData) {
  console.log(book.title) // Autocomplete works!
}