Skip to content

Feature: Support function-based conditions in conditional args#35369

Open
Programer1804 wants to merge 1 commit into
storybookjs:nextfrom
Programer1804:feat/conditional-arg-function
Open

Feature: Support function-based conditions in conditional args#35369
Programer1804 wants to merge 1 commit into
storybookjs:nextfrom
Programer1804:feat/conditional-arg-function

Conversation

@Programer1804

@Programer1804 Programer1804 commented Jul 4, 2026

Copy link
Copy Markdown
Contributor

Closes #19135

What I did

Extended the conditional args system to accept a function as a condition, in addition to the existing object based syntax.

Previously, conditional args only supported simple single condition checks via an object (if: { arg: 'type', eq: 'button' }). Complex scenarios that depend on multiple args simultaneously were impossible to express. This change allows passing a function that receives the current args and globals and returns a boolean

The existing object based syntax continues to work exactly as before, the function check is added before the existing logic, and falls through to the original behavior when the condition is not a function.

Checklist for Contributors

Testing

The changes in this PR are covered in the following automated tests:

  • stories
  • unit tests
  • integration tests
  • end-to-end tests

Manual testing

No manual testing is strictly required, since this is a backward compatible addition fully covered by unit tests. To verify manually:

  1. Run a sandbox: yarn task --task sandbox --start-from auto --template react-vite/default-ts
  2. In a story, add an argType with a function condition, e.g. if: (args) => args.a && args.b

Summary by CodeRabbit

  • New Features

    • Conditional logic now supports function-based rules, allowing more flexible show/hide behavior based on both current values and global settings.
    • Function-based conditions can evaluate multiple checks and receive global context for more advanced decisions.
  • Tests

    • Added coverage for function-based conditional behavior, including matching, non-matching, multiple conditions, and global argument handling.

@github-actions

github-actions Bot commented Jul 4, 2026

Copy link
Copy Markdown
Contributor
Fails
🚫

PR is not labeled with one of: ["cleanup","BREAKING CHANGE","feature request","bug","documentation","maintenance","build","dependencies"]

🚫

PR is not labeled with one of: ["ci:normal","ci:merged","ci:daily","ci:docs"]

🚫

PR is not labeled with one of: ["qa:needed","qa:skip","qa:success"]

Generated by 🚫 dangerJS against 0999bf7

@coderabbitai

coderabbitai Bot commented Jul 4, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

This PR adds support for function-based conditionals in Storybook's argType.if mechanism. A new ConditionalFunction type is introduced, the Conditional type union is extended, and includeConditionalArg now branches to evaluate function predicates with (args, globals), alongside existing tests.

Changes

Function-based Conditional Args

Layer / File(s) Summary
Conditional type contract
code/core/src/csf/story.ts
Adds exported ConditionalFunction type and extends Conditional to accept either the existing object-based shape or a function predicate.
Conditional evaluation logic
code/core/src/csf/includeConditionalArg.ts
Updates type imports and branches includeConditionalArg to call argType.if as a function with (args, globals) when applicable, otherwise retains prior object-based { arg, global } resolution and testValue evaluation.
Function conditional tests
code/core/src/csf/includeConditionalArg.test.ts
Adds tests validating function-based predicates return correct booleans, support multiple/nested conditions, and receive globals as the second argument.

Estimated code review effort: 2 (Simple) | ~10 minutes


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@code/core/src/csf/story.ts`:
- Around line 61-62: The conditional type in story.ts still includes the
function branch, so applying Omit to Conditional in includeConditionalArg.ts
collapses the object shape and weakens testValue typing. Update the type used
for testValue to exclude ConditionalFunction first, or introduce an object-only
alias (for example via Exclude<Conditional, ConditionalFunction>) before Omit so
the truthy/exists/eq/neq structure remains enforced.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: edf7c1cd-2138-4877-aeec-6f58c14a761f

📥 Commits

Reviewing files that changed from the base of the PR and between fac05a5 and 0999bf7.

📒 Files selected for processing (3)
  • code/core/src/csf/includeConditionalArg.test.ts
  • code/core/src/csf/includeConditionalArg.ts
  • code/core/src/csf/story.ts

Comment on lines +61 to +62
export type ConditionalFunction = (args: Args, globals: Globals) => boolean;
export type Conditional = (ConditionalValue & ConditionalTest) | ConditionalFunction;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '\n== files ==\n'
git ls-files 'code/core/src/csf/story.ts' 'code/core/src/csf/includeConditionalArg.ts' || true

printf '\n== outline: story.ts ==\n'
ast-grep outline code/core/src/csf/story.ts --view expanded || true

printf '\n== outline: includeConditionalArg.ts ==\n'
ast-grep outline code/core/src/csf/includeConditionalArg.ts --view expanded || true

printf '\n== relevant excerpts ==\n'
sed -n '1,140p' code/core/src/csf/story.ts
printf '\n---\n'
sed -n '1,120p' code/core/src/csf/includeConditionalArg.ts

printf '\n== type-level probe (best effort) ==\n'
node - <<'JS'
const fs = require('fs');
const path = 'code/core/src/csf/includeConditionalArg.ts';
const src = fs.readFileSync(path, 'utf8');
console.log(src.includes("Omit<Conditional, 'arg' | 'global'>") ? 'found Omit<Conditional, ...>' : 'pattern not found');
JS

Repository: storybookjs/storybook

Length of output: 13811


🏁 Script executed:

#!/bin/bash
set -euo pipefail

rg -n "testValue\\(" code/core/src -g '*.ts' -g '*.tsx'
printf '\n---\n'
rg -n "type ConditionalFunction|type Conditional =" code/core/src/csf/story.ts
printf '\n---\n'
python3 - <<'PY'
# Lightweight structural probe for the TypeScript utility pattern in question.
# This does not type-check the repo; it just documents the set-theoretic expectation.
print("Omit< (object | function), keys-from-object > is typically {} when keyof of the union collapses to never.")
PY

Repository: storybookjs/storybook

Length of output: 946


🏁 Script executed:

#!/bin/bash
set -euo pipefail

node - <<'JS'
let ts;
try {
  ts = require('typescript');
} catch (e) {
  console.log('typescript-not-installed');
  process.exit(0);
}

const source = `
type ConditionalTest = { truthy?: boolean } | { exists: boolean } | { eq: any } | { neq: any };
type ConditionalValue = { arg: string } | { global: string };
type ConditionalFunction = (args: any, globals: any) => boolean;
type Conditional = (ConditionalValue & ConditionalTest) | ConditionalFunction;
type X = Omit<Conditional, 'arg' | 'global'>;
`;
const fileName = 'x.ts';
const host = ts.createCompilerHost({ noEmit: true, strict: true });
host.getSourceFile = (f, v) => f === fileName ? ts.createSourceFile(f, source, v, true) : undefined;
host.readFile = (f) => f === fileName ? source : undefined;
host.fileExists = (f) => f === fileName;
const program = ts.createProgram([fileName], { noEmit: true, strict: true }, host);
const checker = program.getTypeChecker();
const sf = program.getSourceFile(fileName);
const typeAlias = sf.statements.find(s => ts.isTypeAliasDeclaration(s) && s.name.text === 'X');
const type = checker.getTypeAtLocation(typeAlias);
console.log(checker.typeToString(type));
JS

Repository: storybookjs/storybook

Length of output: 195


🏁 Script executed:

#!/bin/bash
set -euo pipefail

node - <<'JS'
let ts;
try {
  ts = require('typescript');
} catch (e) {
  console.log('typescript-not-installed');
  process.exit(0);
}

const source = `
type ConditionalTest = { truthy?: boolean } | { exists: boolean } | { eq: any } | { neq: any };
type ConditionalValue = { arg: string } | { global: string };
type ConditionalFunction = (args: any, globals: any) => boolean;
type Conditional = (ConditionalValue & ConditionalTest) | ConditionalFunction;
type T = Omit<Conditional, 'arg' | 'global'>;

declare let ok1: T;
ok1 = { truthy: true };
ok1 = { exists: false };
ok1 = { eq: 1 };
ok1 = { neq: 1 };
ok1 = {};
ok1 = { foo: 1 };
ok1 = () => true;
`;

const fileName = 'x.ts';
const options = { noEmit: true, strict: true, target: ts.ScriptTarget.ES2020 };
const host = ts.createCompilerHost(options);
host.getSourceFile = (f, v) => f === fileName ? ts.createSourceFile(f, source, v, true) : undefined;
host.readFile = (f) => f === fileName ? source : undefined;
host.fileExists = (f) => f === fileName;
host.writeFile = () => {};

const program = ts.createProgram([fileName], options, host);
const checker = program.getTypeChecker();
const sf = program.getSourceFile(fileName);

const typeAlias = sf.statements.find(s => ts.isTypeAliasDeclaration(s) && s.name.text === 'T');
const tType = checker.getTypeAtLocation(typeAlias);
const props = checker.getPropertiesOfType(tType).map(p => p.name).sort();
console.log('properties:', JSON.stringify(props));
console.log('typeString:', checker.typeToString(tType));

const diags = ts.getPreEmitDiagnostics(program);
for (const d of diags) {
  const msg = ts.flattenDiagnosticMessageText(d.messageText, '\n');
  console.log(`diag:${d.code}:${msg}`);
}
JS

Repository: storybookjs/storybook

Length of output: 15846


🏁 Script executed:

#!/bin/bash
set -euo pipefail

node - <<'JS'
let ts;
try {
  ts = require('typescript');
} catch (e) {
  console.log('typescript-not-installed');
  process.exit(0);
}

const source = `
type Exclude<T, U> = T extends U ? never : T;
type Pick<T, K extends keyof T> = { [P in K]: T[P] };
type Omit<T, K extends keyof any> = Pick<T, Exclude<keyof T, K>>;

type ConditionalTest = { truthy?: boolean } | { exists: boolean } | { eq: any } | { neq: any };
type ConditionalValue = { arg: string } | { global: string };
type ConditionalFunction = (args: any, globals: any) => boolean;
type Conditional = (ConditionalValue & ConditionalTest) | ConditionalFunction;
type T = Omit<Conditional, 'arg' | 'global'>;

declare let t: T;
t = { truthy: true };
t = { exists: false };
t = { eq: 1 };
t = { neq: 1 };
t = {};
t = { foo: 1 };
t = () => true;
`;

const fileName = 'x.ts';
const options = { noEmit: true, strict: true, target: ts.ScriptTarget.ES2020, noLib: true };
const host = ts.createCompilerHost(options);
host.getSourceFile = (f, v) => f === fileName ? ts.createSourceFile(f, source, v, true) : undefined;
host.readFile = (f) => f === fileName ? source : undefined;
host.fileExists = (f) => f === fileName;
host.writeFile = () => {};

const program = ts.createProgram([fileName], options, host);
const checker = program.getTypeChecker();
const sf = program.getSourceFile(fileName);

const typeAlias = sf.statements.find(s => ts.isTypeAliasDeclaration(s) && s.name.text === 'T');
const tType = checker.getTypeAtLocation(typeAlias);
console.log('typeString:', checker.typeToString(tType));
console.log('properties:', JSON.stringify(checker.getPropertiesOfType(tType).map(p => p.name).sort()));

const diags = ts.getPreEmitDiagnostics(program);
for (const d of diags) {
  const msg = ts.flattenDiagnosticMessageText(d.messageText, '\n');
  console.log(`diag:${d.code}:${msg}`);
}
JS

Repository: storybookjs/storybook

Length of output: 15603


Exclude the function branch before applying Omit in code/core/src/csf/includeConditionalArg.ts.

Omit<Conditional, 'arg' | 'global'> collapses to {} because Conditional still includes ConditionalFunction, so testValue no longer enforces the truthy/exists/eq/neq shape. Use Exclude<Conditional, ConditionalFunction> (or an object-only alias) here.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@code/core/src/csf/story.ts` around lines 61 - 62, The conditional type in
story.ts still includes the function branch, so applying Omit to Conditional in
includeConditionalArg.ts collapses the object shape and weakens testValue
typing. Update the type used for testValue to exclude ConditionalFunction first,
or introduce an object-only alias (for example via Exclude<Conditional,
ConditionalFunction>) before Omit so the truthy/exists/eq/neq structure remains
enforced.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Feature Request] functions for conditional controls

1 participant