# Anti‑Patterns to Avoid When Narrowing React Prop Types: A Complete Guide

> Avoid anti-patterns when narrowing React prop types. Discover best practices to maintain type safety and prevent runtime errors in your production code.

- Repository: [HumanLayer/skills](https://github.com/humanlayer/skills)
- Tags: best-practices
- Published: 2026-09-07

---

**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`](https://github.com/humanlayer/skills/blob/main/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`](https://github.com/humanlayer/skills/blob/main/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`](https://github.com/humanlayer/skills/blob/main/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`](https://github.com/humanlayer/skills/blob/main/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:

```tsx
// 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 array
- `onSelect?.()` creates a clickable element that may do nothing
- `items ?? []` silently substitutes missing data
- `defaultExpandedIds` serves Storybook, not production needs

### Correct Narrowed Implementation

```tsx
// 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 `items` and `onSelect`
- **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`](https://github.com/humanlayer/skills/blob/main/SKILL.md):

- `React.ComponentProps<typeof RealComponent>` — extract props from an existing component
- `Parameters<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`](https://github.com/humanlayer/skills/blob/main/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`](https://github.com/humanlayer/skills/blob/main/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`](https://github.com/humanlayer/skills/blob/main/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 `ComponentProps` or `Parameters` helpers

---

## 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.