Default Props and Optional Props in React
How to type optional props in TypeScript and set sensible defaults without reaching for the deprecated defaultProps API.
When I first started using React with TypeScript, I treated every prop as required. If a component needed a label, I made the label mandatory. If it needed a size, I passed a size. My components were honest, but they were also noisy. Every call site had to specify every value, even when the obvious default was staring me in the face.
Then I discovered optional props and default values. The problem was that React has two different ways to set defaults. One is the modern destructuring approach. The other is the defaultProps class component API, which React has deprecated for function components. I mixed them up for longer than I care to admit. This is what I wish I had known on day one.
Typing Optional Props
In TypeScript, you mark a prop as optional by adding a question mark to its key. The compiler then knows the caller can skip it, and the component has to deal with the missing value.
type ButtonProps = {
label: string;
variant?: 'primary' | 'secondary';
disabled?: boolean;
};
function Button({ label, variant, disabled }: ButtonProps) {
return (
<button
className={variant === 'secondary' ? 'btn-secondary' : 'btn-primary'}
disabled={disabled}
>
{label}
</button>
);
}
Here label is required. variant and disabled are optional. TypeScript will complain if you forget the label, but it will happily accept <Button label="Save" /> without the other two.
Inside the component, TypeScript sees variant as string | undefined and disabled as boolean | undefined. Try to use them without a check and the compiler complains. Defaults fix that.
Setting Defaults with Destructuring
The cleanest way to give an optional prop a default value is to assign it directly in the destructuring pattern.
function Button({
label,
variant = 'primary',
disabled = false,
}: ButtonProps) {
return (
<button
className={variant === 'secondary' ? 'btn-secondary' : 'btn-primary'}
disabled={disabled}
>
{label}
</button>
);
}
Now variant is always 'primary' | 'secondary' and disabled is always boolean. TypeScript knows the defaults fill the gaps, so the undefined possibility disappears after destructuring. The types are tighter. You read the signature and you know exactly what happens.
This works for objects and arrays too, but be careful. A default object literal in the destructuring pattern is recreated on every render. If you pass that object down to a memoized child, it will break the memoization.
type ChartProps = {
data: number[];
options?: { color: string; height: number };
};
// This creates a new object reference every render
function Chart({ data, options = { color: 'blue', height: 300 } }: ChartProps) {
// ...
}
If Chart or its children rely on reference equality, pull the default out to module scope instead.
const defaultOptions = { color: 'blue', height: 300 };
function Chart({ data, options = defaultOptions }: ChartProps) {
// ...
}
Why I Stopped Using defaultProps
Class components in React have a static defaultProps property. It used to be the idiomatic way to set defaults, and for a while the same API was supported on function components.
function Button({ label, variant, disabled }: ButtonProps) {
return <button className={variant}>{label}</button>;
}
Button.defaultProps = {
variant: 'primary',
disabled: false,
};
React deprecated defaultProps for function components. It splits the component contract in two: you have to read the type definition, the function signature, and the static property just to understand what the component actually receives. Destructuring keeps everything in one place.
TypeScript also has a harder time inferring types from defaultProps. The static assignment is not checked as tightly as an inline default, and you lose autocomplete inside the component body because TypeScript sees the props as potentially undefined until it notices the defaultProps annotation. Destructuring defaults solve both problems.
Optional Props with Rest Patterns
Sometimes you want to forward most props to an underlying element and only override a few with defaults. The rest pattern makes this straightforward.
type InputProps = {
label: string;
} & React.ComponentPropsWithoutRef<'input'>;
function Input({ label, type = 'text', ...rest }: InputProps) {
return (
<label>
{label}
<input type={type} {...rest} />
</label>
);
}
Here type is optional and defaults to 'text', but every other valid input attribute is accepted through the spread. ComponentPropsWithoutRef gives you the full HTML attribute type, so you do not have to redeclare onChange, placeholder, or className manually.
When Defaults Belong in the Parent Instead
Not every missing prop should have a component-level default. If a prop is conceptually required but the parent often passes the same value, a wrapper component or a factory function is sometimes cleaner than baking the default into the leaf component.
function PrimaryButton(props: Omit<ButtonProps, 'variant'>) {
return <Button {...props} variant="primary" />;
}
This keeps Button honest about what it needs while removing repetition at the call sites. I use this pattern when a design system has a small set of recurring configurations that would otherwise clutter every JSX expression with identical prop values.
What Changed in How I Write Components
I used to think optional props were a form of laziness, a way to avoid specifying everything. Now I see them as part of the component contract. A required prop is a question the parent must answer. An optional prop with a sensible default is a decision the component can make on its own. The fewer questions I force on callers, the more reusable the component becomes. The key is making those defaults visible in the function signature, not hidden in a static property or a separate configuration file.
Newsletter
A weekly newsletter on React, Next.js, AI-assisted development, and engineering. No spam, unsubscribe any time.