How to Generate TypeScript Types for Your Sanity V3 Schema
Automatically generate accurate TypeScript types from your Sanity schema to catch errors early and speed up your development workflow.

Working with typed content from your Sanity Content Lake significantly improves developer experience by enabling autocompletion, catching potential data handling errors early, and making refactoring safer. This guide will walk you through the process of generating TypeScript definitions from your Sanity Studio schemas and GROQ queries using Sanity TypeGen.
Important Note: sanity-codegen
is Deprecated
If you've previously used or heard of sanity-codegen
, please be aware that it is deprecated for Sanity v3. The official and recommended tool for generating types is now Sanity TypeGen, provided by the Sanity team.
Prerequisites
Before you begin, ensure you have the following set up:
-
Sanity CLI: Version 3.35.0 or later. You can check your version with
sanity version
. -
Sanity Studio Project: A functioning Sanity Studio with your schemas defined.
-
Schema Definitions: Your schema types should be correctly defined (e.g., using
defineType
,defineField
). -
sanity.cli.ts
Configuration: Yoursanity.cli.ts
file at the root of your project should be correctly configured with yourprojectId
anddataset
. For example:// sanity.cli.ts import {{ defineCliConfig }} from 'sanity/cli' const projectId = process.env.NEXT_PUBLIC_SANITY_PROJECT_ID const dataset = process.env.NEXT_PUBLIC_SANITY_DATASET export default defineCliConfig({{ api: {{ projectId, dataset }} }}) // Add your studioHost or other configurations as needed
(This example is based on your
sanity.cli.ts
) -
Schema Entry Point: Your schemas should be consolidated and exported using
createSchema
in a central file, typicallyindex.ts
within your schema types directory. For example:// src/sanity/schemaTypes/index.ts import {{ createSchema }} from 'sanity' import {{ blockContentType }} from './blockContentType' import {{ categoryType }} from './categoryType' import {{ postType }} from './postType' import {{ authorType }} from './authorType' import emailSubscription from './emailSubscription' import contactSubmission from './contactSubmission' export const schema = createSchema({{ name: 'default', types: [blockContentType, categoryType, postType, authorType, emailSubscription, contactSubmission], }})
(This example is based on your
src/sanity/schemaTypes/index.ts
)
Type Generation Workflow
The process involves two main steps executed via the Sanity CLI:
Step 1: Extract Your Schema
First, you need to extract your Studio's schema into a static JSON representation. Navigate to your project's root directory (where sanity.cli.ts
is located) and run:
sanity schema extract
This command will:
- Read your schema definitions.
- Output a
schema.json
file in your project root (by default).
You should see a confirmation message:
✔ Extracted schema
This schema.json
file is an intermediate representation that Sanity TypeGen uses in the next step.
Step 2: Generate TypeScript Types
Once you have the schema.json
file, you can generate the TypeScript definitions:
sanity typegen generate
This command will:
- Read the
schema.json
file. - Scan your project (by default,
./src
and its subdirectories) for GROQ queries written using thegroq
template literal ordefineQuery
. - Generate a TypeScript file, typically named
sanity.types.ts
(by default), in your project root. This file contains:- Types for all your Sanity schema documents and objects.
- Types for the results of your GROQ queries.
You should see a confirmation message similar to:
✔ Generated TypeScript types for X schema types and Y GROQ queries in Z files into: ./sanity.types.ts
Understanding the Output
schema.json
: This file is a static representation of your schema. You generally don't need to interact with it directly, but it's crucial for the type generation process. You can add this file to your.gitignore
if you regenerate it as part of your build or development process.sanity.types.ts
: This is the golden file! It contains all the TypeScript definitions. You'll import types from this file into your application code. (Your generatedsanity.types.ts
file will include types likeSanityImageAsset
,Code
,PostQueryResult
, etc., based on your specific schemas and queries.)
Using Generated Types
With sanity.types.ts
generated, you can now strongly type your Sanity data in your frontend or other applications:
- Catch Bugs: TypeScript will help you catch errors if you try to access non-existent properties or use incorrect data types.
- Autocompletion: Enjoy autocompletion for document fields and GROQ query results in your code editor.
- Easier Refactoring: When you change your Sanity schemas, regenerating types will immediately highlight parts of your code that need updating.
Automatic Sanity Client Type Inference
If you use defineQuery
from the groq
package to define your GROQ queries, the Sanity Client (@sanity/client
or next-sanity
) can automatically infer the return types when you use client.fetch()
, provided that your sanity.types.ts
file is included in your tsconfig.json
.
Example:
// your-queries.ts
import { defineQuery } from 'groq';
export const MY_POSTS_QUERY = defineQuery(`*[_type == "post"]{ title, slug }`);
// your-data-fetching-function.ts
import client from './sanityClient'; // Your configured Sanity client
import { MY_POSTS_QUERY } from './your-queries';
async function getPosts() {
const posts = await client.fetch(MY_POSTS_QUERY);
// 'posts' will be automatically typed as MY_POSTS_QUERYResult (or similar)
// posts[0].title -> autocompletes and is type-checked!
return posts;
}
Example: Typing a Fetched Blog Post in BlogPostPage
Let's look at how you can use the generated types in a real component, like your src/app/blog/[slug]/page.tsx
.
Before (Using a Custom Interface):
You might have previously defined a custom interface for your blog post data and used it with your fetching function:
// Potentially a custom interface you defined manually
interface CustomPostInterface {
_id: string;
title?: string;
subtitle?: string;
// ... other fields manually typed
author: { name?: string; /* ... */ };
categories?: string[];
body?: any[]; // For Portable Text
markdownContent?: string;
// etc.
}
// In your BlogPostPage component:
// import { sanityFetch } from 'path/to/your/sanityFetch';
// import { POST_QUERY } from 'path/to/your/queries';
// const post = await sanityFetch<CustomPostInterface>({ // Using the custom interface
// query: POST_QUERY,
// params: { slug },
// tags: ['post']
// });
// if (!post) {
// // Handle post not found
// }
// Accessing data: post.title, post.author.name
After (Using Generated Types):
With sanity.types.ts
generated, you can replace CustomPostInterface
with the type generated specifically for your POST_QUERY
.
-
Import the Generated Type: Your
sanity.types.ts
file will contain a type for yourPOST_QUERY
. Based on your provided file, this is namedPOST_QUERYResult
.import type { POST_QUERYResult } from 'path/to/your/sanity.types'; // Adjust path if needed
-
Using the Generated Type in
sanityFetch
:// src/app/blog/[slug]/page.tsx (relevant part) import { sanityFetch } from 'path/to/your/sanityFetch'; // Assuming you have a utility like this import { POST_QUERY } from 'path/to/your/queries'; // Your GROQ query import type { POST_QUERYResult } from '../../../sanity.types'; // Correct relative path to sanity.types.ts // ... export default async function BlogPostPage({ params, }: { params: Promise<{ slug: string }> // or { slug: string } }) { const { slug } = await params; const post = await sanityFetch<POST_QUERYResult>({ // Use the generated type query: POST_QUERY, params: { slug }, tags: ['post'] }); if (!post) { // ... (post not found handling) // The type of 'post' here is correctly inferred as 'null' if POST_QUERYResult is a union with null return <div>Post not found</div>; } // Now 'post' is typed according to POST_QUERYResult. // For example, post.title, post.author.name, post.body etc. are all type-checked. // jsonLd, header elements, PortableText component, etc., all benefit from these types. // console.log(post.title); // Autocomplete and type safety! // ... rest of your component }
Important Note on POST_QUERYResult
being null
:
In the sanity.types.ts
file you provided, POST_QUERYResult
is currently defined as null
:
// Source: ./src/sanity/lib/queries.ts
// Variable: POST_QUERY
// Query: *[_type == "post" && slug.current == $slug][0] { ... }
export type POST_QUERYResult = null;
This usually means one of two things:
- The query can legitimately return
null
(which*[_type == "post" && slug.current == $slug][0]
does if no post is found), and TypeGen has determined this is the only possible type. - More likely, Sanity TypeGen was unable to fully determine the structure of the post when a post is found, and defaulted to
null
. This can happen if there are complexities in the schema or query that TypeGen doesn't fully support, or if the setup isn't complete (e.g.,sanity.types.ts
not intsconfig.json
's include path during a previous generation).
Ideally, for a query like POST_QUERY
that fetches a single document, the generated POST_QUERYResult
should be a union type representing the structure of the document when found, and null
when not. For example:
// What POST_QUERYResult should ideally look like (simplified)
export type POST_QUERYResult = {
_id: string;
_type: "post";
title?: string;
subtitle?: string;
// ... all other fields from your query projection ...
author?: {
name?: string;
// ... other author fields ...
};
categories?: Array<string>;
} | null;
If your query types are consistently null
or Array<never>
, please:
- Ensure
sanity schema extract
runs successfully beforesanity typegen generate
. - Verify that your GROQ queries are correctly defined (e.g., assigned to variables, using
groq
tag from thegroq
package ordefineQuery
). - Check that
sanity.types.ts
is included in yourtsconfig.json
file'sinclude
array. - Consult the "Troubleshooting & Tips" section below and the official Sanity TypeGen documentation.
By using the (correctly generated) POST_QUERYResult
, you leverage the full power of TypeScript, getting autocompletion and type safety based directly on your Sanity schema and GROQ queries, eliminating the need to maintain manual interfaces.
Configuration (Optional)
While the default settings work well for many projects, you can customize the behavior of sanity typegen generate
by creating a sanity-typegen.json
file in your project root.
Example sanity-typegen.json
with default values:
{
"path": "./src/**/*.{ts,tsx,js,jsx}",
"schema": "schema.json",
"generates": "./sanity.types.ts",
"overloadClientMethods": true
}
path
: Glob pattern(s) for where TypeGen should look for GROQ queries.schema
: Path to yourschema.json
file.generates
: Path for the output TypeScript definitions file.overloadClientMethods
: Set tofalse
to disable automatic Sanity Client method overloading.
Adding a Script to package.json
To streamline the process, you can add a script to your package.json
:
// package.json
"scripts": {{
// ... other scripts
"generate:types": "sanity schema extract && sanity typegen generate"
}},
Then you can run pnpm generate:types
(or npm run
/ yarn
) to perform both steps.
(This matches the script we added to your package.json
.)
Furthermore, to ensure your types are always current before building your application, you can integrate this script into your existing build
script:
// package.json
"scripts": {{
"dev": "pnpm next dev --turbopack",
"generate:types": "sanity schema extract && sanity typegen generate",
"build": "pnpm generate:types && pnpm next build", // Updated build script
"start": "pnpm next start",
"lint": "pnpm next lint"
}},
This way, every time you run pnpm build
, it will first regenerate your Sanity types and then proceed with the Next.js build process.
Troubleshooting & Tips
tsconfig.json
: Ensure your generatedsanity.types.ts
file is included in theinclude
array of your project'stsconfig.json
. For example:// tsconfig.json { "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", "sanity.types.ts"], // ... other configurations }
- Unique Query Names: TypeGen requires all GROQ queries it processes to have unique variable names.
- GROQ Syntax: Ensure your GROQ queries are valid.
- Unsupported Features: While TypeGen supports most schema types and GROQ features, some complex or niche cases might result in
unknown
types. Check the official Sanity documentation for the latest on supported features. - Schema Errors: If TypeGen produces empty types (e.g.,
null
orArray<never>
), incomplete types, or no types for your custom documents, it often indicates an underlying error in your Sanity schema definitions. Runnpx sanity dev
(orsanity dev
) and check the terminal and browser console for errors. The Sanity Studio must load correctly and show all your custom types forsanity schema extract
andsanity typegen generate
to work as expected. Resolving schema errors reported by the Studio is a critical first step. - Query Projections vs. Schema Definitions: If your generated types look off or are missing fields you expect (even if not
null
orArray<never>
), double-check that the fields selected in your GROQ queries (e.g., withindefineQuery
) actually match the fields defined in your corresponding Sanity schemas (likeauthorType.ts
,postType.ts
, etc.). TypeGen generates types based on what your query requests, so if a field isn't in the query's projection, it won't be in the resulting type, regardless of whether it's in the schema. You can freely adjust your query projections to include or exclude fields as needed.
By following this guide, you can effectively leverage Sanity TypeGen to bring strong typing to your Sanity projects, leading to more robust and maintainable code.