Setting up TypeScript Types from Headless CMS Schemas
Generate TypeScript types from your CMS schema instead of hand-maintaining them, or schema drift will bite: an editor adds a subtitle field, the interface stays stale, and you ship either a runtime undefined crash or any casts that erase type safety entirely. The disconnect is worst across multi-environment setups, where staging schemas diverge from production. This guide covers the extraction pipelines — GraphQL codegen and REST/JSON-Schema introspection — plus runtime validation and CI gating that keep types and content models in lockstep. It’s part of Platform Integration Deep Dives.
The Schema Drift Problem
Type instability comes from a timing mismatch: content teams iterate on schemas on a different clock than code deploys, and the frontend gets no signal when a field is renamed, tightened, or re-related. Lacking an extraction pipeline, developers fall back on memory and stale docs — which collapses at agency velocity.
The canonical failure: a required author reference is made optional to support drafts. A component reads author.name, the API returns null, and TypeScript passes because the interface was never updated — TypeError: Cannot read properties of null in production. Treating the CMS schema as the single source of truth, generated automatically, moves that failure to development where it’s cheap.
Automated Type Generation Strategies
The reliable approach generates interfaces straight from the schema registry via an extraction step that runs locally and in CI, using introspection endpoints or exported schema definitions.
GraphQL Codegen Pipeline for Content-First Frameworks
For GraphQL endpoints, @graphql-codegen/cli introspects the live schema and maps it to TypeScript interfaces with strict null checks, so every field, union, and enum matches what the API returns.
// codegen.ts
import type { CodegenConfig } from '@graphql-codegen/cli';
const config: CodegenConfig = {
schema: 'https://graphql.contentful.com/content/v1/spaces/{SPACE_ID}',
documents: ['src/queries/**/*.graphql'],
generates: {
'src/types/cms.generated.ts': {
plugins: ['typescript', 'typescript-operations'],
config: {
strictScalars: true,
scalars: {
DateTime: 'string',
JSON: 'Record<string, unknown>',
},
avoidOptionals: { field: true },
maybeValue: 'T | undefined',
},
},
},
};
export default config;
npx graphql-codegen writes cms.generated.ts mirroring your exact query shapes. The avoidOptionals setting stops accidental undefined propagation by enforcing nullability only where the schema dictates it. The same fragments and generated types stay in sync with the patterns in the Contentful Integration Guide across feature branches.
REST API & JSON Schema Introspection
For REST platforms, fetch the schema definition and transform it. openapi-typescript and json-schema-to-typescript parse OpenAPI specs or raw JSON Schema into .d.ts files per the JSON Schema Specification.
Write a small Node script that authenticates with the management API, pulls the latest content-type definitions, and pipes them through the transformer, mapping CMS field types (richText, slug, link) to their TypeScript equivalents. Commit the output so schema evolution shows up as a reviewable diff.
Bridging Compile-Time Types with Runtime Validation
Generated interfaces exist only at compile time — tsc never checks an actual API response against them. Pair them with a runtime validator: define a schema that mirrors the generated interface, then parse and narrow each response with it. On a validation failure, degrade to a fallback UI instead of crashing. This dual layer — static types for the IDE, runtime schemas for data integrity — is what keeps a corrupted payload from silently rendering into a statically generated page. It also composes with TypeScript’s Utility Types for transformations driven by runtime data.
CI/CD Integration & Schema Drift Prevention
Local generation is half the job; CI has to enforce it or drift still reaches production. The workflow:
- Fetch the latest schema from the staging or production CMS environment.
- Run the type generation script.
- Compare the newly generated file against the committed version using
git diff. - Fail the build if discrepancies are detected, requiring developers to run the generator locally and commit the updated types before merging.
No deploy ships without an explicit acknowledgment of the schema change, which forces editors and developers to coordinate field edits through change requests instead of ad-hoc CMS tweaks. The gate works like this:
flowchart TD
Fetch["Fetch latest schema from CMS"] --> Gen["Run type generation script"]
Gen --> Diff["git diff vs committed types"]
Diff --> Q{"Discrepancies?"}
Q -->|No| Pass["Build proceeds"]
Q -->|Yes| Fail["Fail build"]
Fail --> Local["Regenerate locally and commit"]
Local --> Fetch
Agency & Multi-Environment Workflows
When projects share a CMS instance or template repo, namespace generated types by environment or project to prevent collisions, and keep a per-environment .env pointing the generator at the right space ID and token. Document the generation step in the README and onboarding checklist, and have content teams validate schema changes in a preview environment before promoting to production. Treated as a code-adjacent discipline, content modeling keeps delivery fast without giving up type safety.