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.

·Matija Žiberna·
How to Generate TypeScript Types for Your Sanity V3 Schema

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:

  1. Sanity CLI: Version 3.35.0 or later. You can check your version with sanity version.

  2. Sanity Studio Project: A functioning Sanity Studio with your schemas defined.

  3. Schema Definitions: Your schema types should be correctly defined (e.g., using defineType, defineField).

  4. sanity.cli.ts Configuration: Your sanity.cli.ts file at the root of your project should be correctly configured with your projectId and dataset. 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)

  5. Schema Entry Point: Your schemas should be consolidated and exported using createSchema in a central file, typically index.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 the groq template literal or defineQuery.
  • 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 generated sanity.types.ts file will include types like SanityImageAsset, 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.

  1. Import the Generated Type: Your sanity.types.ts file will contain a type for your POST_QUERY. Based on your provided file, this is named POST_QUERYResult.

    import type { POST_QUERYResult } from 'path/to/your/sanity.types'; // Adjust path if needed
    
  2. 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 in tsconfig.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:

  1. Ensure sanity schema extract runs successfully before sanity typegen generate.
  2. Verify that your GROQ queries are correctly defined (e.g., assigned to variables, using groq tag from the groq package or defineQuery).
  3. Check that sanity.types.ts is included in your tsconfig.json file's include array.
  4. 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 your schema.json file.
  • generates: Path for the output TypeScript definitions file.
  • overloadClientMethods: Set to false 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 generated sanity.types.ts file is included in the include array of your project's tsconfig.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 or Array<never>), incomplete types, or no types for your custom documents, it often indicates an underlying error in your Sanity schema definitions. Run npx sanity dev (or sanity dev) and check the terminal and browser console for errors. The Sanity Studio must load correctly and show all your custom types for sanity schema extract and sanity 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 or Array<never>), double-check that the fields selected in your GROQ queries (e.g., within defineQuery) actually match the fields defined in your corresponding Sanity schemas (like authorType.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.

7
Enjoyed this article?
Subscribe to my newsletter for more insights and tutorials.
Matija Žiberna
Matija Žiberna
Full-stack developer, co-founder

I'm Matija Žiberna, a self-taught full-stack developer and co-founder passionate about building products, writing clean code, and figuring out how to turn ideas into businesses. I write about web development with Next.js, lessons from entrepreneurship, and the journey of learning by doing. My goal is to provide value through code—whether it's through tools, content, or real-world software.

You might be interested in

    How to Generate TypeScript Types for Your Sanity V3 Schema | Build with Matija