Anti‑Patterns to Avoid When Narrowing React Prop Types: A Complete Guide
Never widen prop types for test or story convenience, then tighten implementation—this defeats type safety and introduces hidden runtime guards that don't exist in production.
The narrow-react-prop-types skill in the humanlayer/skills repository defines a disciplined approach to React component contracts. When components are initially built with loose types to accommodate Storybook stories or test mocks, developers must narrow those types back to match real production usage. However, several common anti-patterns sabotage this effort. This guide examines each pattern, explains why it undermines type safety, and shows how to correct it using the official skill documentation and source examples.
Why Narrowing React Prop Types Matters
React components often accumulate optional props and defensive fallbacks during development. These loosened contracts make stories easier to write but create a dangerous gap: the component's type signature no longer reflects how it's actually used in production code. The narrowing process treats live, non-test call sites as the sole source of truth for the component contract, then refactors stories and tests to comply with that stricter definition.
According to the skill source in plugins/narrow-react-prop-types/skills/narrow-react-prop-types/SKILL.md, failing to narrow types properly leads to:
- Runtime defensive code that never executes in production
- Hidden bugs from untested API shapes
- Maintenance overhead from duplicated or inconsistent contracts
Six Critical Anti-Patterns to Avoid
The SKILL.md file enumerates specific anti-patterns that re-introduce width into supposedly narrowed components. Each pattern mixes test or story concerns with the production API.
Making Callbacks Optional So Stories Can Omit Them
Optional callbacks (onAction?: (...) => void) encourage components to guard against undefined at runtime. This spawns defensive branches that never occur when the component is used with real data.
In production, if a button is always interactive, its handler should be required. Making it optional only for story convenience forces the component to handle a "no-op" state that doesn't exist in live code.
Rendering Interactive Elements With Optional Invocation
Using optional chaining for callbacks (onAction?.(...)) inside interactive UI elements creates a dangerous mismatch. The element appears clickable to users, yet the component silently handles cases where nothing happens.
As noted in SKILL.md lines 75-77, this pattern "signals that the UI may be inert, yet the component visually presents an interactive element."
Adding default* Props for Storybook When Live Code Is Controlled
Default props (defaultExpandedIds, defaultValue) mask missing data in stories. They allow components to render with placeholder state that isn't part of the real API.
When live code uses fully controlled components—where parent state drives all values—these defaults hide missing required data and let stories succeed without fixing the underlying contract.
Using Null-Coalescing to Hide Missing Required State
The ?? [] or ?? 0 pattern silently supplies fallback values, effectively re-introducing optionality at the call site. The component no longer guarantees that callers supply real values.
From SKILL.md lines 78-80: "Null-coalescing (??) silently supplies fallback values, effectively re-introducing optionality at the call-site level."
Accepting Multiple API Shapes When Live Code Uses One
Union types or overloaded signatures that accommodate several shapes create wider contracts than applications need. This invites bugs from untested variants that real code never exercises.
If production only passes objects with id and name, don't accept string | { id, name } just because some legacy test uses the simple form.
Treating Pure Components as Mocks With Relaxed Contracts
Pure components should be deterministic. Relaxing their prop types for test scaffolding blurs the line between production and mock code, making correctness harder to reason about.
Code Comparison: Bad vs. Good Patterns
Anti-Pattern Implementation
The following component from the skill documentation demonstrates multiple anti-patterns simultaneously:
// BadComponent.tsx
export interface Props {
items?: string[]; // optional just for Storybook
onSelect?: (id: string) => void; // optional so stories can omit it
defaultExpandedIds?: string[]; // Storybook default
}
export const BadComponent: React.FC<Props> = ({
items,
onSelect,
defaultExpandedIds,
}) => {
// Defensive fallback – hides missing required state
const list = items ?? [];
return (
<ul>
{list.map(i => (
<li key={i} onClick={() => onSelect?.(i)}>
{i}
</li>
))}
</ul>
);
};
Problems in this implementation:
items?allows undefined when production always provides an arrayonSelect?.()creates a clickable element that may do nothingitems ?? []silently substitutes missing datadefaultExpandedIdsserves Storybook, not production needs
Correct Narrowed Implementation
// GoodComponent.tsx
export interface Props {
items: string[]; // required – live code always provides it
onSelect: (id: string) => void; // required – UI element is interactive
}
// Derive type from live API if possible:
// export type Props = React.ComponentProps<typeof RealComponent>;
export const GoodComponent: React.FC<Props> = ({
items,
onSelect,
}) => {
// No fallback – callers must supply a concrete array
return (
<ul>
{items.map(i => (
<li key={i} onClick={() => onSelect(i)}>
{i}
</li>
))}
</ul>
);
};
Key improvements:
- Removed optionality on
itemsandonSelect - Eliminated defensive fallbacks—callers must provide valid data
- Required callback matches the interactive nature of the UI
- Type derivation from real components reduces manual duplication
How to Derive Types From Live Code
The skill emphasizes deriving prop types from actual production usage rather than maintaining parallel definitions. Two techniques from SKILL.md:
React.ComponentProps<typeof RealComponent>— extract props from an existing componentParameters<typeof productionFunction>[0]— derive from real function signatures
This ensures stories and tests stay synchronized with production contracts automatically.
Automating the Narrowing Workflow
The repository includes structured templates for CI agents. The file at plugins/narrow-react-prop-types/skills/narrow-react-prop-types/references/response-template.md defines the expected response format when agents process narrowing tasks.
The skill metadata in plugins/narrow-react-prop-types/.claude-plugin/plugin.json declares how the narrow-react-prop-types skill integrates with the Skills platform, enabling CLI invocation as documented in the root README.md.
Summary
- Never make props optional solely for story or test convenience—match production usage exactly
- Avoid
?.optional calls on callbacks for interactive elements - Eliminate
default*props when live code uses controlled patterns - Remove
??fallbacks that hide missing required state - Don't accept union types wider than production actually uses
- Keep pure components strict—don't relax contracts for mock scaffolding
- Derive types from live code using
ComponentPropsorParametershelpers
Frequently Asked Questions
What does "narrowing" React prop types actually mean?
Narrowing means reducing the set of allowed values in a component's prop types to match exactly what production code provides. It removes optionality, union members, and fallback patterns that were added solely for testing or demonstration purposes. The goal is making the type system enforce real contracts rather than accommodating every possible usage.
Why can't I just keep props optional and use defensive coding?
Defensive branches for undefined values add runtime complexity and hide data flow problems. When production always provides a value, the defensive code never executes—yet it remains, complicating maintenance and masking whether stories actually represent reality. TypeScript's purpose is catching these issues at compile time, not runtime.
How do I fix existing stories after narrowing prop types?
Refactor stories to supply complete, valid data that matches production usage. Use factory functions or fixtures that create realistic prop objects. If deriving types from live components, stories will receive compile-time errors when they're incomplete, guiding you to fix them rather than widening the component contract.
When should I use React.ComponentProps versus defining interfaces manually?
Use React.ComponentProps<typeof Component> when a production component already exists with correct types. Define interfaces manually only for new shared components or when no single source of truth exists. Manual definitions should still be validated against real call sites during the narrowing process.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →