Skip to content

[Bug]: context.globals and story-level globals setting share the same TS type — impossible to have non-optional runtime globals without breaking partial overrides #34951

@svitovyda

Description

@svitovyda

Describe the bug

The Globals interface in storybook/internal/csf is used for two structurally different purposes that require different types:

  1. Story/meta-level globals setting — a partial override that pins specific toolbar controls. A story only sets the fields it cares about:
export const MyStory: StoryObj = {
  globals: { vertical: INSURANCE_CATEGORY.DENTAL }, // partial — intentional
};
  1. Runtime context.globals — the resolved globals object inside a render function or decorator, where all fields are present because defaults are set at the preview level:
(Story, context) => {
  context.globals.locale // should be ACCOUNT_AREA_LOCALE, not ACCOUNT_AREA_LOCALE | undefined
}

Because both use Globals, any project that augments it with typed fields faces a forced choice:

  • Make augmented fields optional → context.globals.myField is MY_FIELD_TYPE | undefined even though defaults are always set, requiring boilerplate defensive code or casts everywhere reads happen.
  • Make augmented fields required → every story that does a partial globals: { vertical: ... } override gets a TS error for missing fields (and setting missing fields blocks them from being changed by the story user).

Neither is correct. The type system offers no way to express "partial when writing, resolved when reading."

Suggested fix

Introduce a distinction in the type signatures:

// In StoryAnnotations / ComponentAnnotations
globals?: Partial<TGlobals>; // partial override

// In StoryContext
globals: TGlobals; // fully resolved

CSF4's definePreview / defineStory typed chain could naturally carry this distinction, but the underlying Globals interface and StoryContext should reflect it regardless of whether a project uses CSF3 or CSF4.

Reproduction link

https://codesandbox.io/p/sandbox/storybook-globals-repro-53hmv7

Reproduction steps

Add your custom global variable myField: MY_FILED_TYPE, and try to implement TS types that make each of your stories properly typed.

Workaround currently required:

Projects must maintain two separate interfaces manually:

// For context reads — non-optional, project-defined
interface StorybookGlobals {
  myField: MY_FILED_TYPE;
}

// For story writes — keep Globals augmentation optional
declare module 'storybook/internal/csf' {
interface Globals {
    myField?: MY_FILED_TYPE;   // optional to allow partial story overrides
  }
}

// Then cast useGlobals() at the read site
const [, updateGlobals] = useGlobals() as [unknown, (g: Partial<StorybookGlobals>) => void];
const globals = getStorybookGlobals(context); // custom helper that fills defaults

This duplication is error-prone and should not be necessary.

Expected behaviour

Story-level globals should be typed as Partial<Globals> (or a dedicated GlobalsOverride type), the same way story args are a partial of the component's full args type. context.globals should be typed as Globals (non-optional fields), reflecting that the preview has resolved all defaults before the render function is called.

This mirrors a pattern Storybook already gets right for args: Meta.args accepts Partial<TArgs>, while context.args in a render function gives the full resolved TArgs.

System

System:
    OS: macOS 26.5
    CPU: (14) arm64 Apple M4 Pro
    Shell: 5.9 - /bin/zsh
  Binaries:
    Node: 24.14.1 - ~/.volta/tools/image/node/24.14.1/bin/node
    npm: 11.11.0 - ~/.volta/tools/image/node/24.14.1/bin/npm <----- active
  Browsers:
    Chrome: 148.0.7778.216
    Safari: 26.5
  npmPackages:
    @storybook/addon-a11y: 9.1.20 => 9.1.20 
    @storybook/addon-docs: 9.1.20 => 9.1.20 
    @storybook/addon-onboarding: 9.1.20 => 9.1.20 
    @storybook/react-vite: 9.1.20 => 9.1.20 
    eslint-plugin-storybook: 9.1.20 => 9.1.20 
    msw-storybook-addon: 2.0.5 => 2.0.5 
    storybook: 9.1.20 => 9.1.20

Additional context

No response

Metadata

Metadata

Assignees

No one assigned

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions