Optional Props and Default Values in React TypeScript
Optional props and defaults are essential for building flexible React components that work in multiple contexts without forcing callers to pass every possible prop. TypeScript marks optional props with a ? suffix in the interface, while JavaScript provides defaults via destructuring or null coalescing—together, these patterns let you write components that adapt to different use cases while maintaining type safety.
Declaring Optional Props with the ? Suffix
In a prop interface, append ? to mark a prop as optional. Optional props may be undefined:
// Modal.tsx
interface ModalProps {
title: string; // required
message: string; // required
isOpen: boolean; // required
onClose: () => void; // required
footer?: React.ReactNode; // optional
width?: number; // optional
closeOnBackdrop?: boolean; // optional
}
const Modal: React.FC<ModalProps> = ({
title,
message,
isOpen,
onClose,
footer,
width = 500,
closeOnBackdrop = true,
}) => {
if (!isOpen) return null;
const handleBackdropClick = () => {
if (closeOnBackdrop) onClose();
};
return (
<div className="modal-backdrop" onClick={handleBackdropClick}>
<div
className="modal"
style={{ width: `${width}px` }}
onClick={(e) => e.stopPropagation()}
>
<h2>{title}</h2>
<p>{message}</p>
{footer && <div className="modal-footer">{footer}</div>}
<button onClick={onClose}>Close</button>
</div>
</div>
);
};
export default Modal;
Callers can now omit the optional props:
// All valid:
<Modal title="Confirm" message="Delete?" isOpen={true} onClose={() => {}} />
<Modal title="Confirm" message="Delete?" isOpen={true} onClose={() => {}} footer={<button>Delete</button>} width={600} />
Providing Default Values with Destructuring
Set default values directly in the destructuring parameter. TypeScript infers the type from the default:
// Pagination.tsx
interface PaginationProps {
currentPage: number;
totalPages: number;
onPageChange: (page: number) => void;
maxVisible?: number;
showArrows?: boolean;
}
const Pagination: React.FC<PaginationProps> = ({
currentPage,
totalPages,
onPageChange,
maxVisible = 5,
showArrows = true,
}) => {
// maxVisible is number (never undefined)
// showArrows is boolean (never undefined)
const startPage = Math.max(1, currentPage - Math.floor(maxVisible / 2));
const endPage = Math.min(totalPages, startPage + maxVisible - 1);
const pages = Array.from({ length: endPage - startPage + 1 }, (_, i) => startPage + i);
return (
<nav className="pagination">
{showArrows && (
<button onClick={() => onPageChange(currentPage - 1)} disabled={currentPage === 1}>
← Previous
</button>
)}
{pages.map((page) => (
<button
key={page}
className={page === currentPage ? 'active' : ''}
onClick={() => onPageChange(page)}
>
{page}
</button>
))}
{showArrows && (
<button onClick={() => onPageChange(currentPage + 1)} disabled={currentPage === totalPages}>
Next →
</button>
)}
</nav>
);
};
export default Pagination;
Inside the component, maxVisible and showArrows are never undefined—they always have a value. This is safer than checking for undefined.
Handling Undefined Optional Props
Optional props that have no default are undefined inside the component. Handle them safely with conditional checks:
// Avatar.tsx
interface AvatarProps {
name: string;
image?: string;
size?: 'small' | 'medium' | 'large';
}
const Avatar: React.FC<AvatarProps> = ({ name, image, size = 'medium' }) => {
const sizeClass = {
small: 'w-8 h-8',
medium: 'w-12 h-12',
large: 'w-16 h-16',
}[size];
return (
<div className={`avatar ${sizeClass}`}>
{image ? (
<img src={image} alt={name} />
) : (
<div className="initials">{name.charAt(0)}</div>
)}
</div>
);
};
export default Avatar;
The ternary operator handles the case where image is undefined—show initials as a fallback.
Optional Chaining for Undefined Properties
Use optional chaining (?.) to safely access properties on optional props:
// ExpandablePanel.tsx
interface PanelContent {
title: string;
description?: string;
nested?: {
level: number;
items: string[];
};
}
interface ExpandablePanelProps {
content: PanelContent;
metadata?: {
createdAt: Date;
author?: string;
};
}
const ExpandablePanel: React.FC<ExpandablePanelProps> = ({ content, metadata }) => {
return (
<div className="panel">
<h3>{content.title}</h3>
{content.description && <p>{content.description}</p>}
{/* Safely access nested properties */}
{content.nested?.items && (
<ul>
{content.nested.items.map((item) => <li key={item}>{item}</li>)}
</ul>
)}
{/* Safely access optional metadata */}
{metadata && (
<footer>
Created: {metadata.createdAt.toLocaleDateString()}
{metadata.author && ` by ${metadata.author}`}
</footer>
)}
</div>
);
};
export default ExpandablePanel;
The ?. operator prevents "Cannot read property of undefined" errors. If the property doesn't exist, the expression short-circuits and evaluates to undefined.
Nullish Coalescing for Fallback Values
Use the nullish coalescing operator (??) to provide a fallback when a prop is null or undefined:
// Badge.tsx
interface BadgeProps {
label: string;
count?: number;
variant?: 'primary' | 'secondary';
}
const Badge: React.FC<BadgeProps> = ({ label, count, variant }) => {
// Use ?? to fall back when count is null or undefined
const displayCount = count ?? 0;
// Use ?? to fall back variant
const badgeVariant = variant ?? 'primary';
return (
<span className={`badge badge-${badgeVariant}`}>
{label}
{displayCount > 0 && <span className="count">{displayCount}</span>}
</span>
);
};
export default Badge;
Unlike ||, the ?? operator only triggers on null or undefined, not on falsy values like 0 or "". This is crucial when 0 is a valid count value.
Comparison: Defaults in Destructuring vs. Function Body
You can set defaults either in the destructuring parameter (preferred) or in the function body:
| Approach | Code | Trade-offs |
|---|---|---|
| Destructuring | { maxVisible = 5 } = props | Clean, idiomatic, type inference, preferred |
| Function body | const maxVisible = props.maxVisible || 5; | Visible in body, allows conditional logic |
| Nullish coalescing | const x = props.x ?? 5; | Only for null/undefined, not other falsy values |
Destructuring defaults are preferred because they're concise, appear in the function signature (documenting expectations), and TypeScript infers that the variable is never undefined.
Optional Event Handlers
Mark event handler props as optional to support components used in different contexts:
// Button.tsx
interface ButtonProps {
label: string;
onClick?: (e: React.MouseEvent<HTMLButtonElement>) => void;
onFocus?: (e: React.FocusEvent<HTMLButtonElement>) => void;
onBlur?: (e: React.FocusEvent<HTMLButtonElement>) => void;
}
const Button: React.FC<ButtonProps> = ({
label,
onClick,
onFocus,
onBlur,
}) => (
<button
onClick={onClick}
onFocus={onFocus}
onBlur={onBlur}
>
{label}
</button>
);
export default Button;
React's JSX automatically handles undefined event handlers, so no guard is needed. The component works whether or not handlers are provided.
Key Takeaways
- Mark props as optional with
?to allow callers to omit them from the interface. - Provide defaults via destructuring defaults syntax (
{ count = 0 }) to ensure a value always exists inside the component. - Use optional chaining (
?.) to safely access properties on optional props without null-coalescing checks. - Use nullish coalescing (
??) to provide fallback values only when the prop isnullorundefined, not other falsy values. - Optional event handler props allow flexibility—React safely handles undefined handlers without errors.
Frequently Asked Questions
What is the difference between ? suffix and | undefined in a prop type?
prop?: string is shorthand for prop: string | undefined. Both mean the prop is optional. The ? syntax is simpler and conventional; | undefined is more explicit.
When should I use default values vs. handling undefined in the function?
Use defaults when a sensible fallback exists. Use undefined handling (conditional rendering) when the component behavior fundamentally changes. Defaults are cleaner for options like size = 'medium'; undefined handling is better when the component should hide/show content.
Why use ?? instead of || for defaults?
The ?? operator only triggers on null or undefined, not on falsy values like 0, false, or "". Use ?? when these values are legitimate; use || only for string fallbacks or when you want to ignore all falsy values.
Can I make a prop both optional and have a required sub-property?
Yes—metadata?: { id: number; name: string } means the metadata prop itself is optional, but if provided, it must have both id and name. See React Props Interfaces: Define Custom Component Types for nested interface examples.
Is it safe to assume an optional prop with a default is never undefined?
Yes—if you set a default in the destructuring, TypeScript infers the variable is never undefined. Inside the function, you can use it without guards. However, if you do not set a default, the prop is T | undefined and needs guards.