Generating TypeScript Types for Storyblok Components

This guide, part of Storyblok Visual Editor Integration, sets up generated TypeScript types for every Storyblok block, so the frontend’s components and the space’s schema stay in agreement. It covers pulling component schemas with the Storyblok CLI, generating types, typing block components and the component registry, checking for drift in CI, and handling fields that editors may leave empty.

Storyblok’s schema lives in the space, not in the codebase: developers and sometimes content designers add fields and blocks in the Storyblok app. Without generated types, the frontend’s interfaces for those blocks are written by hand and drift from the space as soon as someone renames a field or changes an option list. The mismatch shows up as empty sections on pages, not as errors. Generating types from the schema turns each such change into a type error in the component that renders the block.

From space schema to typed componentsThe CLI pulls component schemas from the staging space into a JSON file; the type generator turns them into a TypeScript file with one type per block; block components and the registry import those types; CI repeats the pull and generation and fails when the committed file differs.Storyblok spacecomponentsCLI pullcomponents.jsonType generationstoryblok.types.tsBlock componentsTyped registry
The space is the source of truth; the generated file is its reviewed snapshot.

The Problem

An online education company had forty blocks with hand-written interfaces. A content designer changed the cta block’s link field from a plain text URL to a multilink, so editors could pick stories. The frontend still read blok.link as a string and rendered [object Object] in the href of every call-to-action on the site. The change passed review in Storyblok, where nobody was looking at code, and the frontend had no way of knowing until users reported broken buttons.

How Type Generation Works

Pull schemas. The Storyblok CLI, authenticated with a personal access token, pulls the space’s component definitions into JSON files. Recent CLI versions provide a components pull command; older versions used pull-components.

Generate types. The CLI’s type generator reads the pulled schemas and writes a TypeScript file with one type per block, including the field types, option values, nested block fields and Storyblok’s common properties such as _uid and component. Recent versions provide types generate; older ones used generate-typescript-typedefs.

Use them everywhere. Block components take their generated type as props, the registry is typed so every block name maps to a component that accepts that block, and fetch helpers type the root content of each story.

Check in CI. CI pulls schemas from the staging space, regenerates types and fails if the committed file differs, then runs the type checker. A schema change without matching frontend work fails the pipeline.

What generated types catchSchema changes in Storyblok and whether generated types turn them into compile errors: renamed fields, changed field types, new option values, removed blocks and new blocks without components.Schema changeCaught at compile time?Field renamedyesField type changedyesOption added to a listyes, if switch is exhaustiveBlock removedyes, registry entry errorsNew block without componentwith a typed registry
Most silent schema breakages become compile errors; a registry check covers new blocks.

Implementation

Add scripts that pull schemas and generate types, using a management token from the environment:

JSON
{
  "scripts": {
    "sb:pull": "storyblok components pull --space $STORYBLOK_SPACE_ID",
    "sb:types": "storyblok types generate --space $STORYBLOK_SPACE_ID",
    "sb:sync": "npm run sb:pull && npm run sb:types",
    "typecheck": "tsc --noEmit"
  }
}

Check the CLI’s help for the exact commands and output paths of your version, and move or re-export the generated file to a stable location such as src/storyblok/storyblok.types.ts, so imports do not depend on the CLI’s defaults.

Block components use the generated types as props:

TSX
// components/blocks/Cta.tsx
import { storyblokEditable } from "@storyblok/react/rsc";
import type { CtaStoryblok } from "@/storyblok/storyblok.types";
import { hrefFor } from "@/lib/links";

export default function Cta({ blok }: { blok: CtaStoryblok }) {
  const href = hrefFor(blok.link);        // multilink type: a string here is a compile error
  if (!href || !blok.label) return null;  // optional in the schema, so handle it
  return (
    <a {...storyblokEditable(blok)} href={href} className={`cta cta--${blok.variant ?? "primary"}`}>
      {blok.label}
    </a>
  );
}

A typed registry makes the mapping from block names to components exhaustive:

TypeScript
// components/blocks/registry.ts
import type { ComponentType } from "react";
import type * as T from "@/storyblok/storyblok.types";
import Cta from "./Cta";
import Hero from "./Hero";
import Teaser from "./Teaser";

// Every block type the space defines, keyed by its component name.
type Blocks = { cta: T.CtaStoryblok; hero: T.HeroStoryblok; teaser: T.TeaserStoryblok };

export const registry: { [K in keyof Blocks]: ComponentType<{ blok: Blocks[K] }> } = {
  cta: Cta,
  hero: Hero,
  teaser: Teaser,
};

To make the registry catch new blocks, add a small test that reads the pulled components JSON and asserts that every nestable block has an entry in the registry. The test fails when a block is added in Storyblok without a component, which is exactly when a developer should be involved.

TypeScript
// tests/registry.test.ts
import components from "@/storyblok/components.json";
import { registry } from "@/components/blocks/registry";

test("every nestable block has a component", () => {
  const nestable = (components as { name: string; is_nestable: boolean }[]).filter((c) => c.is_nestable).map((c) => c.name);
  const missing = nestable.filter((name) => !(name in registry));
  expect(missing).toEqual([]);
});

Running it in CI

Run sb:sync in CI against the staging space with a read-only management token, then git diff --exit-code on the generated files, then the type checker and tests. Run the same job on a schedule, not only on pull requests, because schema changes in Storyblok do not create pull requests: a nightly run that fails on drift tells the team that the space has moved ahead of the code.

Schema changes as a process

Types turn silent breakage into visible failures, but the smoother path is a process where schema changes and component changes land together. Make schema changes in a development space first, pull and generate types on a branch, update components, and apply the schema to staging and production when the frontend change is deployed. Storyblok’s CLI can push component definitions to another space, which keeps the schema itself under version control.

Configuration Reference

Item Recommendation Why
Source space staging, pulled in CI Types match what will ship.
Token read-only management token in CI Least privilege.
Output committed, stable path Reviewable diffs.
Components typed props from generated types Field changes fail compilation.
Registry typed, plus a completeness test New blocks need components.
Schedule nightly drift check Space changes without pull requests.

Gotchas & Edge Cases

  • Everything optional. Fields that are not required in the schema are optional in the types; handle undefined rather than asserting, since editors can leave them empty.
  • Required fields added later. Existing content does not gain values when a field becomes required; the type says required, the data may not. Validate at the data boundary for critical fields.
  • Rich text and multilinks. Their generated types are structures, not strings; use the shared helpers for rendering and links.
  • Custom field plugins. Plugin fields may be typed loosely; define their shapes in a small hand-written extension.
  • Several spaces. When sites use different spaces with shared blocks, generate types per space and share only blocks whose schemas are identical.

Worked Example

The education company introduced generated types, a typed registry and the nightly drift check. On the first run, the type checker reported 58 errors, including the broken call-to-action and eleven other components reading fields that no longer existed or had changed type. After fixing them, schema changes started to arrive with frontend changes in the same week, because the nightly check made drift visible the morning after a change. In the following six months, no broken block reached production because of a schema mismatch.

Issues found on the first type checkType errors found when generated types first replaced hand-written interfaces, split into real rendering bugs, missing handling of optional fields, and harmless differences.Real rendering bugs12 type errorsUnhandled optional fields31 type errorsHarmless differences15 type errors
Hand-written interfaces had been hiding a dozen real rendering bugs.

Beyond Types: Validating Content

Types describe the schema; they cannot promise that every story’s content matches it. Old stories may lack fields added later, imported content may contain unexpected values, and custom field plugins may store data in shapes the generator cannot express. For pages where a broken block is costly, such as checkout or pricing, validate block data at the boundary with a runtime schema library and render a safe fallback when validation fails, logging the story and block _uid so editors can fix the content. Keep such runtime validation focused on the few blocks that matter; for most blocks, generated types plus defensive handling of optional fields are enough.

Rollout Checklist

  • Pull component schemas from the staging space with the CLI.
  • Generate types and commit them at a stable path.
  • Type block components and the registry with generated types.
  • Test that every nestable block has a component.
  • Check for drift in CI on pull requests and nightly.
  • Handle optional fields and validate critical blocks at runtime.

Frequently Asked Questions

Which space should types come from?

The one that represents what will be deployed next, usually staging. Production may lag behind; development may contain experiments.

Should the pulled JSON be committed?

Yes, alongside the types. It feeds the registry test and shows schema changes in pull request diffs.

Do types cover story-level fields?

Generated block types cover content; Storyblok’s story properties such as slugs and dates come from the SDK’s own types.

Can types be generated without the CLI?

Yes, from the Management API’s component endpoint with a custom script, but the CLI keeps the output consistent with Storyblok’s conventions.

How often should types be regenerated locally?

Whenever you pull schema changes or start work on a block. A watch script is unnecessary, since the schema changes far less often than the code that renders it.