Sanity TypeGen: Optimal Project Structure and Production Workflow (2025 Update)
Scale TypeGen from basic setup to production-ready system with optimal project structure, advanced query organization, and bulletproof development workflow
When I first wrote about generating TypeScript types for Sanity V3 schemas, I covered the basics of getting TypeGen up and running. That guide focused on the fundamental setup - extracting schemas, generating types, and using them in components. For production-ready implementation patterns like webhooks and markdown support, solid TypeGen configuration is essential. It was a solid foundation, but as my Sanity projects grew more complex, I discovered that the basic setup wasn't enough.
I recently rebuilt a blog platform where I had dozens of GROQ queries, complex sorting requirements, and a team of developers who needed consistent type safety. The basic TypeGen approach from my previous article worked, but I kept running into organizational challenges: queries scattered across files breaking TypeGen's analysis, dynamic query builders that couldn't be typed, import path inconsistencies causing build failures, and a general lack of structure that made the codebase hard to maintain.
This guide is the evolution of that original article - taking TypeGen from a basic setup to a production-ready system with optimal project structure, advanced query organization, and a bulletproof development workflow. If you followed my previous guide, this will show you how to scale it up. If you're starting fresh, this gives you the complete, battle-tested approach from day one.
Understanding TypeGen at Scale
Sanity TypeGen works by scanning your codebase for GROQ queries and generating TypeScript types based on your project's schema. While this sounds straightforward, the reality is that your project structure and query organization directly impact TypeGen's effectiveness. Poor organization leads to incomplete type generation, build failures, and a frustrating development experience.
The key insight I've learned is that TypeGen isn't just a tool you run - it's a system that needs to be architected properly. Your file structure, query patterns, and development workflow all need to work together to get the full benefits of automatic type generation.
Optimal Project Structure
After testing various approaches across multiple projects, this structure provides the best developer experience and maintainability:
This organization solves several critical issues. First, centralizing all queries in a single file makes them easy for TypeGen to discover and analyze. Second, keeping all Sanity-related code under src/lib/sanity/ creates clear boundaries and consistent import paths. Third, the dedicated types folder provides a clean interface for consuming generated types throughout your application.
The structure also scales well - as you add more queries, they go into the central queries file. As you add more schema types, they get organized in the schemaTypes folder. The clear separation of concerns makes the codebase maintainable even as it grows.
Essential Configuration Files
Your sanity.cli.ts configuration at the project root needs to be complete for TypeGen to work properly:
This configuration allows TypeGen to connect to your Sanity project and extract the schema information it needs. Without proper project connection, TypeGen can't understand your schema structure and will generate incomplete or incorrect types.
Your tsconfig.json must include the generated types file for TypeScript to recognize the generated types:
The sanity.types.ts inclusion is crucial - without it, TypeScript won't recognize the generated types, and you'll lose all the autocomplete and type checking benefits that make TypeGen valuable.
Writing Production-Ready Queries
The most important requirement for reliable TypeGen is using the defineQuery function with proper variable naming. Here's the pattern that works consistently across all project sizes:
Each defineQuery call creates a query that TypeGen can analyze and generate types for. The variable names become the basis for the generated type names - POST_QUERY becomes POST_QUERYResult, POSTS_QUERY becomes POSTS_QUERYResult. This naming convention is predictable and makes it easy to know which types correspond to which queries.
Notice how the queries include comprehensive field selections. TypeGen generates types based on what your queries actually request, not what's available in your schema. If you don't select a field in your query, it won't appear in the generated type, regardless of whether it exists in your schema definition.
Solving Dynamic Query Problems
One critical issue that breaks TypeGen's static analysis is dynamic query generation. Functions that build queries at runtime can't be analyzed by TypeGen:
TypeGen can't analyze this function because it doesn't know what the final query will look like at runtime. The solution is to create static queries for each variation you need:
This approach gives you the best of both worlds. Each static query gets proper type generation from TypeGen, while the helper function provides the runtime flexibility your application needs. Your components can call getPostsQuery(sortBy) and get back a properly typed query, while TypeGen can analyze each individual query variant.
The key insight is separating the concerns: static queries for TypeGen analysis, and helper functions for runtime selection. This pattern scales well - you can add new query variations by creating new static queries and updating the helper function.
Production Type Generation Workflow
Add these scripts to your package.json for a reliable development and deployment workflow:
json
{"scripts":{"generate:types":"sanity schema extract && sanity typegen generate","dev":"pnpm generate:types && next dev --turbopack","build":"pnpm generate:types && next build","types:watch":"sanity typegen generate --watch","types:check":"tsc --noEmit"}}
The type generation process involves two steps that must run in sequence. First, sanity schema extract reads your schema definitions and creates a schema.json file. Then, sanity typegen generate analyzes both your schema and queries to generate the final TypeScript types.
Running generate:types before development and builds ensures your types are always current. This prevents the common problem of running with stale types that don't match your current queries or schema.
For active development, the types:watch script regenerates types automatically when you modify queries. This provides immediate feedback when you change query structures, helping you catch type errors as soon as they're introduced.
The types:check script validates that all your TypeScript code compiles correctly with the generated types. This is particularly useful in CI/CD pipelines to catch type errors before deployment.
Creating Clean Type Exports
Generated types can be verbose and difficult to import directly. Create a clean export system that makes the types more developer-friendly:
typescript
// src/lib/sanity/types/index.tsimporttype {
POST_QUERYResult,
POSTS_QUERYResult,
ENHANCED_POSTS_QUERYResult,
ENHANCED_POSTS_FILTERED_QUERYResult,
POSTS_BY_PUBLISHED_ASC_QUERYResult,
POSTS_BY_DATE_MODIFIED_DESC_QUERYResult,
POSTS_BY_TITLE_ASC_QUERYResult,
CATEGORY_QUERYResult,
ENHANCED_CATEGORY_QUERYResult,
AUTHORS_QUERYResult,
COMMAND_QUERYResult,
COMMENTS_QUERYResult,
} from'../../../../sanity.types'// Re-export all query result typesexporttype {
POST_QUERYResult,
POSTS_QUERYResult,
ENHANCED_POSTS_QUERYResult,
ENHANCED_POSTS_FILTERED_QUERYResult,
POSTS_BY_PUBLISHED_ASC_QUERYResult,
POSTS_BY_DATE_MODIFIED_DESC_QUERYResult,
POSTS_BY_TITLE_ASC_QUERYResult,
CATEGORY_QUERYResult,
ENHANCED_CATEGORY_QUERYResult,
AUTHORS_QUERYResult,
COMMAND_QUERYResult,
COMMENTS_QUERYResult,
// Add new query result types here as needed
} from'../../../../sanity.types'// Re-export schema types with cleaner namesexporttype {
PostasSanityPost,
CategoryasSanityCategory,
AuthorasSanityAuthor,
CommentasSanityComment,
UsefulCommandasSanityCommand,
BlockContentasSanityBlockContent,
SlugasSanitySlug,
SanityImageAsset,
SanityImageHotspot,
SanityImageCrop,
} from'../../../../sanity.types'// Convenient type aliases for common usage patternsexporttypeBlogPost = NonNullable<POST_QUERYResult>
exporttypeBlogPostPreview = POSTS_QUERYResult[0]
exporttypeCategoryDetails = NonNullable<CATEGORY_QUERYResult>
exporttypeCategoryWithPosts = NonNullable<ENHANCED_CATEGORY_QUERYResult>
exporttypeAuthorProfile = AUTHORS_QUERYResult[0]
exporttypeCommandDetails = NonNullable<COMMAND_QUERYResult>
// Utility types for common patternsexporttypeWithRequiredSlug<T> = T & {
slug: {
current: string
}
}
exporttypePostWithRequiredFields = WithRequiredSlug<BlogPost> & {
title: stringpublishedAt: string
}
This export system provides multiple layers of convenience. The direct re-exports give you access to the exact generated types when you need them. The renamed schema types provide cleaner names for general use. The type aliases handle common patterns like null-checking and array element access.
The utility types at the bottom provide reusable patterns for common requirements, like ensuring required fields exist or adding constraints to generated types. This approach scales well as your application grows and you discover more common type patterns.
Using Generated Types in Components
With proper setup, you can now use fully typed Sanity data throughout your application:
The generated types provide complete autocomplete support and catch type errors at compile time. When you modify queries, TypeScript immediately highlights any components that need updates, making refactoring safe and efficient.
Notice how the component uses the helper function getPostsQuery(filters.sortBy) to get the appropriate static query, then types the result with the specific query result type. This pattern gives you both runtime flexibility and compile-time type safety.
Avoiding Common Import Path Issues
During setup and scaling, you'll likely encounter module resolution errors. The key is establishing consistent import paths from the beginning and applying them systematically:
Inconsistent import paths will cause build failures, especially in production where the build process is more strict about module resolution. The solution is to establish a clear pattern early - in this case, everything Sanity-related lives under @/lib/sanity/ - and stick to it consistently.
When you encounter import errors, resist the temptation to fix them one-off. Instead, establish the correct pattern and fix all instances at once. This prevents the same issue from recurring and keeps your codebase consistent.
Production Development Workflow
The complete workflow I use for developing with TypeGen at scale:
Start development: Run pnpm dev (automatically generates types first)
Modify queries: Edit GROQ queries in your centralized queries file
Regenerate types: Run pnpm generate:types or use watch mode during active development
Update components: TypeScript will highlight any components that need changes due to type modifications
Validate types: Run pnpm types:check to ensure all TypeScript code compiles correctly
Build: pnpm build includes type generation automatically for production
This workflow ensures types are always current and catches integration issues immediately. The automatic type generation removes the manual overhead of maintaining type definitions while providing better type safety than any manual approach could achieve.
For team development, consider running type generation in a pre-commit hook to ensure all team members are working with current types. This prevents the common issue of one developer's query changes breaking another developer's code due to stale types.
Advanced Configuration Options
For larger projects, you may want to customize TypeGen's behavior with a sanity-typegen.json configuration file:
The path option controls where TypeGen looks for queries. If you organize queries differently than the default, update this to match your structure. The overloadClientMethods option enables automatic type inference when using client.fetch(), which can be helpful for simpler use cases but may conflict with explicit typing in larger applications.
Production Deployment Considerations
For production deployments, include type generation in your build process. Most platforms handle this automatically if you include it in your build script, but consider these optimization strategies:
Option 1: Generate types during build (slower builds, always current types)
json
{"scripts":{"build":"pnpm generate:types && pnpm next build"}}
Option 2: Pre-generate and commit types (faster builds, risk of stale types)
json
{"scripts":{"build":"pnpm next build"}}
For most applications, Option 1 is safer because it ensures types are always current. The build time increase is usually minimal compared to the risk of deploying with incorrect types. For more context on deployment patterns, see my guide on keeping your Sanity blog static in Next.js 15.
Consider adding generated files like schema.json to .gitignore since they're regenerated on each build. However, you may want to commit sanity.types.ts to make it easier to debug type-related issues in production.
Conclusion
Implementing Sanity TypeGen with proper project structure and workflow eliminates the maintenance overhead of manual type definitions while providing superior type safety and developer experience. The key principles are organizing your queries for TypeGen's static analysis, avoiding dynamic query generation, establishing consistent import patterns, and creating a reliable development workflow.
This production-ready approach scales from small projects to large applications with dozens of queries and multiple developers. The structured organization makes the codebase maintainable, the comprehensive typing catches errors early, and the automated workflow removes manual type maintenance entirely.
The investment in proper setup pays dividends immediately through better autocomplete, safer refactoring, and fewer runtime type errors. As your Sanity implementation grows, this foundation will continue to provide value without requiring significant maintenance or rework.
Let me know in the comments if you have questions about scaling TypeGen to your specific use case, and subscribe for more practical development guides.