[Typegen] Generate named, reusable types for projected (expanded) schema types
Nobody has claimed this yet.
Assessment
- Difficulty
- 5/5
- Estimated time
- Over a week
- Newbie friendliness
- 35/100
- Issue type
- Feature
- Clarity
- Mostly clear
- Activity status
- Stale
- Tech stack
- typescript
- Domain
- tooling
Research direction
Start by tracing how typegen handles schema-defined types, GROQ projections, and defineQuery calls; the issue does not name specific files or tests. Determine how a fragment or projection could produce a named expanded result type independent of a document query, then verify that reusable component types cover dereferenced references without relying on a specific query result.
Written by the indexing model from the issue text.
Description
Is your feature request related to a problem? Please describe.
Typegen generates clean, named, reusable types for schema-defined array/object types (e.g., Showreel, Content), but these types are unusable by the components that render them because they contain unexpanded references ({ _ref: string }). The actual data these components receive comes from GROQ queries that dereference with ->, producing a completely different, unnamed, inline type that's welded to a specific query result.
This means there is no way to type a reusable component that renders a reusable block.
For example, given a showreel array type used across multiple document types (home, page, etc.), the generated schema type is:
// Generated from the schema — clean, named, reusable
export type Showreel = Array<
| { image?: AccessibleImage; _type: "showreelImage"; _key: string }
| { video?: Video; _type: "showreelVideo"; _key: string }
| { reference?: CaseStudyReference; _type: "showreelCaseStudy"; _key: string }
| { reference?: ArticleReference; _type: "showreelArticle"; _key: string }
>;
But a component rendering this data can't use this type because the data it receives from any query has expanded references — full document objects instead of { _ref } pointers. The only way to get a type matching the expanded shape is to extract it from a specific query result:
type ShowreelData = NonNullable<NonNullable<HomeQueryResult>["showreel"]>;
This couples the component's type to a specific document query. If the showreel field is removed from the home document, the type breaks — even though the showreel block still exists on other documents and the component is still in use. Deleting an entire document type is even worse: every type extracted from that query result breaks.
Describe the solution you'd like
Typegen should generate named, reusable types for the expanded (post-projection) shape of schema types, not just the stored shape. When a GROQ fragment is used in a defineQuery call and produces a projected type, that type should be extractable as a standalone named type — independent of the document it was queried from.
One possible approach: allow defineQuery (or a new API like defineFragment or defineProjection) to generate a named result type from a GROQ partial without requiring a complete document query:
// Hypothetical API
export const showreelFragment = defineFragment(`
showreel[] {
_type == "showreelImage" => { ..., image { ..., asset-> } },
_type == "showreelVideo" => { ..., video { ..., asset-> } },
_type == "showreelCaseStudy" => { ..., reference-> { ..., image { ..., asset-> }, video { ..., asset-> }, tags[]-> } },
_type == "showreelArticle" => { ..., reference-> { ..., image { ..., asset-> }, video { ..., asset-> }, tags[]-> } }
}
`);
// Generates: ShowreelFragmentResult — a named, reusable type with expanded references
This type could then be used by the component directly, independent of any document.
Describe alternatives you've considered
-
Extracting from a query result (
NonNullable<HomeQueryResult>["showreel"]): Works but is fragile. The component's type is coupled to an arbitrary document query that could change independently. -
A shape-only
defineQuery: Writing a complete GROQ query that exists purely to generate a type (e.g.,*[_type == "home"][0].showreel[] { ... }). This is a hack — the query is never executed, and it still references a specific document type. -
Handwritten types: Manually authoring types that match the expanded shape. This works well in practice but defeats the purpose of typegen and is what typegen is meant to replace.
-
Making components CMS-agnostic with granular primitive props, moving all Sanity-specific type knowledge to the data layer. This is architecturally sound but introduces significant mapping boilerplate and complexity for what should be a straightforward pattern.
Additional context
This is the most basic CMS component pattern: a reusable content block, used across multiple documents, rendered by a shared component. Every Sanity project of any complexity hits this — showreels, hero sections, CTAs, content blocks, anything with references that gets reused.
Typegen generates types that are either too raw (schema types with unexpanded references) or too specific (query result types tied to individual documents). There is nothing in between, and that gap makes typegen incomplete for real-world component architectures.
- Dominant language
- TypeScript
- Stars
- 1
- Forks
- 2
- Avg merge
- 3d 11h
- Merged PRs (30d)
- 5
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
More from sanity-io/codegen
-
Difficulty 3/5 1-2 days Newbie friendliness 74/100
-
Difficulty 5/5 Over a week Newbie friendliness 42/100
-
Difficulty 3/5 1-2 days Newbie friendliness 48/100
-
Difficulty 4/5 3-5 days Newbie friendliness 45/100
-
Difficulty 5/5 Over a week Newbie friendliness 35/100
All issues in sanity-io/codegen
Similar issues
-
clawsweeper:fix-shape-clear clawsweeper:queueable-fix clawsweeper:source-repro impact:ux-friction issue-rating: 🦞 diamond lobster no-stale P3
Difficulty 2/5 1-3 hours Newbie friendliness 78/100
-
community first-timers-only good first issue hacktoberfest help wanted low hanging fruit up-for-grabs
Difficulty 1/5 Under an hour Newbie friendliness 76/100
-
code-quality refactoring
Difficulty 2/5 1-3 hours Newbie friendliness 84/100
github/gh-aw-firewall#8816 ·
-
integration:quickjs org:external priority:backlog topic:code-interpreter topic:middleware type:feature
Difficulty 2/5 1-3 hours Newbie friendliness 74/100
langchain-ai/deepagents#6450 ·
-
Difficulty 1/5 Under an hour Newbie friendliness 88/100
vercel/react-tweet#225 ·