Feature: Support function-based conditions in conditional args#35369
Feature: Support function-based conditions in conditional args#35369Programer1804 wants to merge 1 commit into
Conversation
|
📝 WalkthroughWalkthroughThis PR adds support for function-based conditionals in Storybook's ChangesFunction-based Conditional Args
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. Comment |
There was a problem hiding this comment.
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
📒 Files selected for processing (3)
code/core/src/csf/includeConditionalArg.test.tscode/core/src/csf/includeConditionalArg.tscode/core/src/csf/story.ts
| export type ConditionalFunction = (args: Args, globals: Globals) => boolean; | ||
| export type Conditional = (ConditionalValue & ConditionalTest) | ConditionalFunction; |
There was a problem hiding this comment.
🎯 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');
JSRepository: 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.")
PYRepository: 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));
JSRepository: 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}`);
}
JSRepository: 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}`);
}
JSRepository: 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.
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 currentargsandglobalsand returns a booleanThe 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:
Manual testing
No manual testing is strictly required, since this is a backward compatible addition fully covered by unit tests. To verify manually:
yarn task --task sandbox --start-from auto --template react-vite/default-tsif: (args) => args.a && args.bSummary by CodeRabbit
New Features
Tests