TypeScript type transformation for Sanity CMS: Automatically resolving references and removing system keys in dynamic page builder components
The Challenge
We decided to migrate our CMS from Contentful to Sanity, this to achieve substantial reductions in our monthly operational costs. While Contentful's pricing model had become misaligned with our content requirements, Sanity's structure allows us to maintain predictable, fixed operational expenses regardless of scale. Although the migration required a significant upfront investment in developer resources, this strategic move has positioned us for long-term cost efficiency and better aligns our infrastructure costs with our business needs.
When building dynamic page builders with Sanity CMS, developers face a fundamental type safety problem. Sanity's TypeGen generates types based on raw data storage format, including unresolved references and internal system metadata. However, your frontend components need clean, resolved data structures.
This creates a critical mismatch between generated types and actual runtime data, forcing developers to choose between type safety and maintainability.
Understanding the Problem
The Page Builder Context
Modern content management demands flexibility. In our system, Sanity CMS powers a modular page builder that gives content creators complete creative freedom:
Component Library: Pre-built, reusable components (team lists, content sections, CTAs)
Drag & Drop Interface: Content creators arrange components in any order
Dynamic Configuration: Each component's content and settings are fully customizable
Unlimited Combinations: Every page becomes unique and unpredictable
This flexibility creates two levels of technical complexity:
Multi-level Nesting: Components contain other components (e.g., TeamMemberList → TeamMemberItem → Link)
Runtime Uncertainty: Pages don't know which components will be present until runtime
TypeGen Limitations
Sanity's TypeGen has two critical limitations for page builders:
1. Incomplete Nested Types (Schema based generation)
When dealing with nested components, TypeGen generates internalGroqTypeReferenceTo markers instead of actual resolved types:
Your frontend receives resolved data, but TypeScript doesn't understand the relationship between references and actual content.
2. System Field Pollution (Schema based generation)
Generated types include Sanity's internal system fields (_createdAt, _rev, _weak, etc.) that your frontend components don't need and shouldn't handle.
GROQ Query Limitations (Query based generation)
When using conditional queries like _type == "accordion" => @->{ ... }, TypeGen cannot determine which specific component type will be returned at runtime. This creates union type ambiguity where TypeScript loses track of nested data structures.
The Solution: Advanced Type Transformation
Our solution leverages TypeScript's advanced type system to automatically transform raw Sanity types into clean, fully-typed objects that match your actual runtime data.
Key Benefits
Type Safety: Compile-time guarantees for clean data structures
Zero Runtime Overhead: Pure compile-time type transformation
Developer Experience: Work with predictable, clean APIs
Automatic Reference Resolution: No more dealing with _ref objects
Preserved Relationships: Maintains semantic meaning while improving usability
Implementation Deep Dive
Step 1: Define System Keys to Remove
This union type acts as a "blacklist" of internal Sanity properties that should be excluded from our clean frontend types.
Step 2: Reference Detection
This conditional type serves as a type-level predicate, identifying which properties represent Sanity references that need resolution.
Step 3: Reference Type Extraction
This type performs three critical operations:
Extracts the reference key using infer U
Maps to actual types via ReferenceTypeMap lookup
Returns resolved type instead of reference placeholder
Step 4: Optional Property Preservation
This utility maintains the optional/required nature of properties during transformation—essential for preserving the original API contract.
Step 5: The Core Transformation Engine
This recursive conditional type orchestrates the entire transformation:
Array Handling
Recursively cleans each array element while preserving array structure.
Reference Resolution
Detects Sanity references and replaces them with their resolved types.
Object Transformation
For objects, this:
Filters system keys using mapped type key remapping
Resolves references to their actual types
Preserves optionality from the original schema
Recurses deeply through all nested properties
Primitive Pass-through
Returns primitive types unchanged.
Step 6: Reference Type Mapping
This centralized mapping bridges Sanity's internal references with your TypeScript types, ensuring:
Type Safety: References resolve to correct types
Maintainability: All mappings in one location
Extensibility: Easy to add new content types
Real-World Transformation Example
Before: Raw Sanity Types
After: Clean Transformation
Transformation Results
The clean type demonstrates several key improvements:
System fields removed: No more _id, _createdAt, _updatedAt, _rev
References resolved: Full type definitions instead of _ref objects
Optionality preserved: Optional fields remain optional (icon?, iconPosition?)
Nested structure maintained: Deep resolution through multiple levels
Type safety guaranteed: Full IntelliSense and compile-time checking
Usage in Components
With clean types, your React components become more maintainable and type-safe:
Impact on Development Workflow
This type transformation system fundamentally improves the developer experience:
Performance Benefits
Zero runtime cost: All transformations happen at compile-time
Smaller bundle size: No runtime type checking or transformation libraries
Better tree-shaking: Cleaner types improve dead code elimination
Developer Experience
Predictable APIs: Components work with clean, expected data structures
Better IntelliSense: Full autocomplete for nested properties
Compile-time safety: Catch reference resolution errors during build
Cleaner code: No manual casting or type guards needed
Maintainability
Centralized mapping: All reference types managed in one location
Automatic updates: Adding new content types requires minimal changes
Clear separation: Clean boundary between CMS data layer and application logic
Conclusion
This TypeScript type transformation approach solves the fundamental mismatch between Sanity's generated types and frontend application needs. By leveraging advanced TypeScript features, we create a zero-cost abstraction that provides clean, type-safe data structures while maintaining the flexibility of a dynamic page builder system.
The solution demonstrates how sophisticated type-level programming can solve real-world problems, creating better developer experiences without sacrificing performance or maintainability. For teams building complex, dynamic content systems with Sanity CMS, this pattern provides a robust foundation for scalable, type-safe frontend development.
Resources
Sanity Typegen: https://www.sanity.io/docs/apis-and-sdks/sanity-typegen
Sanity PageBuilder: https://www.sanity.io/learn/course/page-building/create-page-builder-schema-types
Read more
Recycle metals with us
Recycle metals with us
Our metals
- Aluminum: the economic impact of recycling
- Copper: uncovering the sustainable scrap cycle
- Lead: the journey of scrap metal recycling towards a greener future
- Magnesium: scrap as a renewable resource for circular economy
- Nickel: scrap recycling as a value maximizer for sustainable industry growth
- Zinc: the sustainable promise of scrap recycling
Look into the future
Look into the future
"How to" guides
"How to" guides
Business with METYCLE
Want to stay updated via WhatsApp?
Stay ahead with METYCLE's products! Join our WhatsApp broadcast channel to receive real-time updates on available metals and unbeatable deals. Explore our current product offerings and never miss out on the best opportunities!
© 2025 METYCLE