[Typegen] Generate named, reusable types for projected (expanded) schema types

Open
#84 0 comments 0 reactions 0 assignees View on GitHub

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

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

More from sanity-io/codegen

All issues in sanity-io/codegen

Similar issues

More TypeScript issues

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.