Dynamic Zones
Dynamic Zones are one of Strapi's most powerful features, allowing content editors to compose pages from a set of predefined components. strapi-typed-client fully supports Dynamic Zones with generated union types, enabling type-safe access to heterogeneous content blocks.
Generated Types
Component interfaces describe the component shape itself — they intentionally do not include __component. The discriminator belongs to the place where the component is used, not the component definition. For every component that appears in at least one Dynamic Zone, the generator emits an extra *Dz alias that adds the __component literal. Dynamic Zone fields are typed as a union of those *Dz aliases.
For example, given a Landing content type with a content Dynamic Zone that allows HeroBlock, TextBlock, and GalleryBlock components:
// Generated output in types.ts
export interface HeroBlock {
id: number
heading: string
subheading: string | null
}
export interface TextBlock {
id: number
body: string
alignment: 'left' | 'center' | 'right'
}
export interface GalleryBlock {
id: number
title: string | null
}
export type HeroBlockDz = HeroBlock & { __component: 'landing.hero-block' }
export type TextBlockDz = TextBlock & { __component: 'landing.text-block' }
export type GalleryBlockDz = GalleryBlock & {
__component: 'landing.gallery-block'
}
export interface Landing {
id: number
documentId: string
title: string
createdAt: string
updatedAt: string
}Populatable fields — media, relations, nested components — live in LandingGetPayload rather than the base Landing, so the content field appears on populated payloads:
type LandingWithContent = LandingGetPayload<{ populate: { content: true } }>
// LandingWithContent['content'] is (HeroBlockDz | TextBlockDz | GalleryBlockDz)[]Why two interfaces per component?
The same component can be used both inside a Dynamic Zone and as a regular type: "component" attribute. Strapi only accepts __component in Dynamic Zones — for regular component attributes the discriminator is invalid and triggers a 400 on write. Keeping it on a separate *Dz alias lets the generator give both contexts the right input/output shapes.
Loading Dynamic Zone Content
Dynamic Zone fields are not populated by default in Strapi responses. You must explicitly populate them to receive the component data:
const landing = await strapi.landing.find({
populate: {
content: true,
},
})For nested relations within Dynamic Zone components, use the on discriminator to specify per-component populate options:
const landing = await strapi.landing.find({
populate: {
content: {
on: {
'landing.hero-block': {
populate: { backgroundImage: true },
},
'landing.gallery-block': {
populate: { images: true },
},
},
},
},
})The return type is fully inferred — populated relations within each component will include the full type instead of just { id, documentId }.
You can also combine on with fields to select specific fields per component:
const landing = await strapi.landing.find({
populate: {
content: {
on: {
'landing.hero-block': {
fields: ['heading', 'subheading'],
populate: { backgroundImage: true },
},
'landing.text-block': {
fields: ['body'],
},
},
},
},
})WARNING
Without populating the Dynamic Zone field, it will be null in the response even if the entry has blocks configured in Strapi.
Type Narrowing
Since each component carries a __component string literal, you can use it as a discriminant to narrow the union type.
Using the __component field
The most reliable approach is to check the __component field directly:
const landing = await strapi.landing.find({
populate: { content: true },
})
if (landing.data?.content) {
for (const block of landing.data.content) {
switch (block.__component) {
case 'landing.hero-block':
// TypeScript knows `block` is HeroBlock here
console.log(block.heading, block.subheading)
break
case 'landing.text-block':
// TypeScript knows `block` is TextBlock here
console.log(block.body, block.alignment)
break
case 'landing.gallery-block':
// TypeScript knows `block` is GalleryBlock here
console.log(block.title, block.images.length)
break
}
}
}Using property checks
You can also narrow using in checks on unique properties, though this is less precise:
if ('heading' in block) {
// block is narrowed to HeroBlock (if `heading` is unique to it)
console.log(block.heading)
}TIP
Prefer __component checks over property checks. The __component field is always present and guaranteed to be unique, whereas property names may overlap across components.
Type guard helpers
For reusable narrowing logic, create type guard functions. Use the *Dz alias as the predicate target so the discriminator is preserved through filtering:
type LandingBlock = LandingGetPayload<{
populate: { content: true }
}>['content'][number]
function isHeroBlock(block: LandingBlock): block is HeroBlockDz {
return block.__component === 'landing.hero-block'
}
function isTextBlock(block: LandingBlock): block is TextBlockDz {
return block.__component === 'landing.text-block'
}
// Usage
const heroes = landing.data?.content?.filter(isHeroBlock) ?? []
// heroes is typed as HeroBlockDz[]Rendering Dynamic Zones in React
A common pattern for rendering Dynamic Zones in a frontend framework:
import type { LandingGetPayload } from '@myapp/strapi-types'
type LandingBlocks = LandingGetPayload<{
populate: { content: true }
}>['content']
const componentMap: Record<string, React.FC<any>> = {
'landing.hero-block': HeroSection,
'landing.text-block': TextSection,
'landing.gallery-block': GallerySection,
}
function DynamicZone({ blocks }: { blocks: LandingBlocks }) {
if (!blocks) return null
return (
<>
{blocks.map((block, index) => {
const Component = componentMap[block.__component]
if (!Component) return null
return <Component key={index} {...block} />
})}
</>
)
}Input Types for Dynamic Zones
The generator emits *DzInput aliases for every component used in a Dynamic Zone. A Dynamic Zone input field is typed as (BlockADzInput | BlockBDzInput | ...)[] — each element requires __component so Strapi can route the block to the right component type:
await strapi.landings.create({
data: {
title: 'New Landing Page',
content: [
{
__component: 'landing.hero-block',
heading: 'Welcome',
subheading: 'Get started today',
},
{
__component: 'landing.text-block',
body: 'This is the introduction paragraph.',
alignment: 'center',
},
],
},
})Dynamic Zone vs regular component attributes
__component is required only for Dynamic Zone payloads. For a regular type: "component" attribute (single or repeatable), Strapi already knows the component type from the schema and will reject __component in the payload with a 400. The generator reflects this: *Input for a regular component field does not include __component, while *DzInput for a Dynamic Zone block does.
When updating, you replace the entire Dynamic Zone array. Strapi does not support partial updates to individual blocks within a Dynamic Zone:
await strapi.landings.update(documentId, {
data: {
content: [
{
__component: 'landing.hero-block',
heading: 'Updated Heading',
subheading: null,
},
// Only these blocks will exist after the update
],
},
})Nullable Behavior
- A Dynamic Zone field is
nullwhen it has no blocks or has not been populated. - An empty Dynamic Zone (all blocks removed) returns as an empty array
[]. - Individual component fields within the zone follow their own nullability rules based on the Strapi schema.