Implementation Patterns7 min read

How to Use Sanity TypeGen for End-to-End Type Safety

The bug that ships to production this way is almost always the same one.

Published July 22, 2026

The bug that ships to production this way is almost always the same one. A backend editor renames a field, deprecates a reference, or turns a string into an array of blocks, and nothing complains until a component three routes deep reads `product.price` and gets `undefined`. The frontend was typed by hand against a shape the CMS quietly stopped returning. Tests pass, the build is green, and the failure surfaces as a blank card in front of a customer.

Sanity attacks this at the root with TypeGen, its schema-to-TypeScript codegen. The idea is straightforward: your content model already lives in code as `defineType` schemas, so the types your frontend expects should be derived from that same source of truth rather than transcribed by a human who will eventually get it wrong. This matters because Sanity is the Content Operating System for the AI era, an intelligent backend where structure lives in your repo and content lives in Content Lake, and TypeGen is the seam that keeps those two facts honest with each other.

This guide walks the actual workflow, from `sanity schema extract` through query-level result types generated from `defineQuery`, and shows why inferring the shape of the query you actually wrote beats typing the whole document you didn't ask for.

The failure mode: hand-written types that drift from the schema

Most type safety in a headless stack is aspirational. A team stands up a content backend, writes a set of TypeScript interfaces that describe what they believe the API returns, and wires their components against those interfaces. For a week, the types are accurate. Then the model changes. Someone adds a variant field, splits an author into a reference, makes a previously required field optional, or renames `heroImage` to `hero`. The compiler keeps compiling, because the compiler has no idea the backend moved. The interface is just a promise a developer made in the past, and nobody renews it.

This is drift, and it is the quiet tax on every content project that types its API by hand. The cost is not the initial typing effort; it is the ongoing reconciliation nobody schedules. Every schema change is a manual audit of which frontend types now lie. In practice that audit does not happen, so the lies accumulate until one of them becomes a runtime error in front of a user. The worst part is that the error looks like a frontend bug, so a frontend engineer spends an afternoon confirming that no, the data really did change shape upstream.

The structural fix is to stop writing the types at all. If the schema is the source of truth, the types should be a build artifact derived from it, regenerated whenever the schema moves. That is the entire premise of TypeGen: your `defineType` schemas are code, they already describe every field, reference, and validation, so the TypeScript your frontend consumes can be generated from them rather than remembered. The promise you made in the past stops mattering, because the promise is now recomputed on every change.

Schema as the single source of truth: Model your business in code

The reason TypeGen works cleanly is upstream of TypeGen. In Sanity, the content model is not a set of definitions you click together in a GUI that lives on a vendor's servers; it is code you commit. A blog post, a product, a landing-page section, and every field on them are declared with `defineType` in your repository, versioned alongside the frontend that reads them. This is the Model your business pillar in practice: the shape of your content is expressed in the same language, the same repo, and the same review process as the application, so a schema change and the code that depends on it move through one pull request.

Content Lake makes this safe by decoupling structure from storage. Schema lives in code, content lives in the cloud, and you can change one without breaking the other. When you add a field, existing documents do not have to be migrated in lockstep; the store simply does not have that field yet on old records, and your generated types will reflect that reality as optional or absent. This is what lets schema-as-code plus TypeGen keep the frontend's expected shape genuinely in sync with what the backend stores, rather than in sync with what a developer once assumed it stored.

Because the model is code, TypeGen has something concrete to read. It does not have to introspect a running API and guess at nullability; it reads the schema definitions directly. That is a categorical difference from workflows where the model definitions live in a platform and the types are inferred at the SDK or transport layer after the fact. Here the type generator and the schema author are looking at the same file.

The workflow: extract, then generate

The mechanics are two commands. First, `sanity schema extract` reads your `defineType` schemas and serializes them into a schema JSON file, a static, machine-readable description of every type in your project. This is the intermediate representation TypeGen consumes. Second, `sanity typegen generate` reads that schema JSON, along with the GROQ queries it can find in your codebase, and emits a TypeScript file. By default the output lands at `./sanity.types.ts`, a single generated module you import from and never edit by hand.

Configuration lives in one of two places, and both are valid. You can add a `typegen` field to your `sanity.cli.ts`, keeping the codegen settings next to the rest of your CLI configuration, or you can drop a standalone `sanity-typegen.json` at the project root if you prefer to isolate it. Either way you point TypeGen at your schema file and tell it where to write output. For active development, the `--watch` flag keeps the generated types current as you edit schemas and queries, so the types regenerate in the background rather than going stale between manual runs.

The important discipline is where these commands sit. Run `schema extract` and `typegen generate` in the same step of your build or as a pre-commit and CI check, and drift becomes structurally impossible to merge: if a schema change alters a type your frontend depends on, the regenerated `sanity.types.ts` changes, and the code that consumed the old shape stops compiling in the same pull request that introduced the change. The manual reconciliation audit from the first section is now the compiler's job, and the compiler does not skip it to hit a deadline.

Query-level result types: typing the shape you actually asked for

The part that separates TypeGen from a schema-only type generator is that it types individual GROQ queries, not just documents. TypeGen discovers queries you write with `defineQuery`, imported from the `groq` package, and infers the exact shape of what each one projects. The return type reflects the fields, references, filters, slices, and dereferences you actually asked for, not the entire document you did not.

Consider a real hybrid-retrieval query. Written with `defineQuery`, a product search filters on type, category, price, and warehouse, then runs a `score()` pipeline that blends `boost([title] match text::query($queryText), 2)` with `text::semanticSimilarity($queryText)`, orders by `_score desc`, slices `[0...10]`, and projects a specific set of fields including a dereferenced `"stock": stockLocation->{ name, available }`. TypeGen reads that query and generates a result type whose shape is exactly those projected fields: `_id`, `title`, `price`, and a `stock` object with `name` and `available`. The dereference is followed and typed; the fields you left out do not appear. Your component receives a value it can trust, field by field, because the type is a computed consequence of the query text.

This is where GROQ's design pays off twice. You already ask for exactly the shape you need in one round trip, with projections, references, and filters expressed inline. TypeGen simply makes the type match that shape. There is no separate operation-typing step and no risk that the type describes a richer document than the query returned. Change the projection, drop a field, add a dereference, and the generated type changes with it on the next run.

Wiring it into delivery: Power anything with types intact

Type safety that stops at the query is only half the value. The point of generating a result type per query is that the type flows all the way to the edge of your application. You import the generated type for `PRODUCT_SEARCH_QUERY` from `sanity.types.ts`, pass the query string to your Sanity client, and the client's response is typed as the exact projected shape. From there the type propagates through your data-fetching layer, into props, into the component that renders a result card. When a required field is missing from the projection, the render code will not compile against the generated type; you find out at build time, in the editor, next to the code that would have crashed.

This is the Power anything pillar with the safety rails on. Content Lake serves the same typed content to any frontend you build, a Next.js app, an Astro site, a Remix route, a native client, and TypeGen gives each of them the same generated contract. Because the types derive from the schema and the query rather than from a per-framework SDK, the guarantee does not weaken when you add a second delivery channel. The shape is defined once, in code, and every consumer inherits it.

Structured, typed output also matters downstream of humans. When an agent or automation reads content, a schema-shaped response the caller can pass straight through beats a wall of prose it has to re-narrate, badly. The same typed projections that keep a React component honest keep a Function, an App SDK surface, or an agent tool honest, because each one is handed objects with known fields rather than text it has to parse. Typing the query is what makes the content safe to hand to anything, not just to a page.

Where TypeGen fits against other type-generation approaches

Plenty of headless platforms generate TypeScript, so the honest comparison is not whether types exist but what they describe. Contentful generates types with cf-content-types-generator, which produces entry skeletons, helper types, and type guards for its JS SDK, and many teams pair GraphQL Code Generator against the GraphQL Content API. That types the content model and the operations you write, but the model definitions live in the platform rather than your codebase, and result shapes are typed at the SDK or GraphQL layer rather than inferred from a code-first query primitive. Strapi ships a `ts:generate-types` CLI command that emits declarations for content types and components, with experimental auto-generation on schema changes; again, that types the model, not the return shape of an arbitrary projected query.

GraphQL-first CMSes like Hygraph lean on GraphQL Code Generator against the schema, which types the operations you author but couples you to GraphQL's fixed round-trip and resolver model rather than a single GROQ projection. Payload is TypeScript-native and self-hosted and generates types straight from collection configs defined in code, which is strong on schema types; its query layer simply works differently from GROQ projection-shape inference.

The precise Sanity difference is twofold. First, the model is code in the same repo as the frontend, so schema and consumers move together through review. Second, TypeGen infers the result type of each query registered with `defineQuery`, so projections, filters, slices, and dereferences map to exact types, not to the shape of the whole document. That is the seam competitors leave for you to close by hand, and closing it by hand is exactly where drift creeps back in.

TypeScript type generation across headless content platforms

FeatureSanityContentfulStrapiHygraph
Where the content model livesIn code as portable `defineType` schemas, committed to the same repo as the frontend and reviewed in the same pull request.Model definitions live in the platform (GUI or CLI), synced to code via generated skeletons rather than authored in the repo.Content types and components are defined in the Strapi project and admin, then declarations are generated from them.Schema is managed in the Hygraph project; the GraphQL schema is the artifact your codegen reads.
How schema types are produced`sanity schema extract` serializes schemas to JSON, then `sanity typegen generate` emits `sanity.types.ts`; a `--watch` flag keeps them current.cf-content-types-generator produces entry skeletons, helper types, and type guards for the JS SDK, kept in sync via CI or webhooks.`ts:generate-types` CLI generates declarations for all content types and components, with experimental auto-generation on change.GraphQL Code Generator runs against the GraphQL schema to produce types for your operations.
Typing a query's exact result shapeTypeGen infers the return type of each `defineQuery`, so projections, filters, slices, and dereferences map to exact types, not the whole document.Result shapes are typed at the SDK layer or via GraphQL codegen on the operations you write, not inferred from a code-first query primitive.Generated declarations cover the content model; there is no built-in inference of an arbitrary projected query's return shape.Types the GraphQL operations you write, though within GraphQL's fixed round-trip and resolver model rather than a single GROQ projection.
Dereferenced fields in the result typeA projection like `"stock": stockLocation->{ name, available }` is followed and typed as a `stock` object with exactly those fields.Linked entries resolve through the SDK or GraphQL; the joined shape is typed at that layer rather than by projecting inline in the query.Relations are typed on the model; the shape returned depends on populate options resolved at request time.Nested relations are typed via the GraphQL selection set you author for each operation.
Round trips to get the shape you needOne GROQ query asks for exactly the fields, references, and filters you want, and TypeGen types that single response.GraphQL selection sets fetch in one request; REST SDK access may require additional include or link resolution.REST and GraphQL both available; populate depth and multiple requests can be needed for deep relations.GraphQL selection sets fetch nested data in one request, bounded by the schema and resolver design.
Drift protection in CIRun extract and generate in CI; a schema change that breaks a consumed type fails the build in the same pull request that caused it.Achievable by wiring the generator into CI or webhooks, but the model lives outside the repo so sync is a step you maintain.Achievable by running the CLI in CI; experimental auto-generation reduces manual runs but is not yet the default guarantee.Achievable by running GraphQL codegen in CI against the deployed schema, keeping types tied to a published schema state.

Ready to try Sanity?

See how Sanity can transform your enterprise content operations.