27.3 Typing Props: Required, Optional, and Defaults with Destructuring
Right, let’s talk about props. They’re the lifeblood of your components, the parameters of your UI functions. And in TypeScript, we get to move from the wild west of “I hope this object has the right stuff” to the rigorously patrolled border of “I will not let you pass without the correct paperwork.”
The core concept is the interface (or a type alias, but I prefer interface for objects—it feels more intentional). This is where you define the contract for your component.
interface WelcomeBannerProps {
userName: string;
messageCount: number;
isLoggedIn: boolean;
}
This interface says, “To use the WelcomeBanner component, you must provide these three things, with these exact types.” Forget to pass userName? TypeScript will politely (or not so politely, depending on your error theme) yell at you. This is your first line of defense against props-related chaos.
The Beauty (and Necessity) of the Optional ?
Now, let’s be realistic. Not every prop is a matter of life and death. Sometimes you have settings, flags, or decorative elements that have sensible defaults or are just… optional. This is where the question mark earns its keep.
interface UserCardProps {
name: string;
title: string;
avatarUrl?: string; // Look at that ?. Glorious.
isVerified?: boolean;
}
The ? modifier tells TypeScript, “Hey, it’s cool if this isn’t provided.” This is infinitely better than the old JavaScript pattern of props.avatarUrl || 'default.png' because TypeScript now knows it might be undefined and will force you to handle that possibility. It turns a runtime surprise into a compile-time thought exercise.
Default Props: The Destructuring Shortcut
You’ve defined an optional prop. Now what? The most elegant and common way to handle its potential absence is to provide a default value right in the function parameter destructuring. It kills two birds with one stone: it satisfies TypeScript’s type checker and gives you a sensible value to work with.
function UserCard({ name, title, avatarUrl = '/default-avatar.png', isVerified = false }: UserCardProps) {
return (
<div>
<img src={avatarUrl} alt={`${name}'s avatar`} />
<h2>{name} {isVerified && <VerifiedBadge />}</h2>
<p>{title}</p>
</div>
);
}
See what we did there? avatarUrl = '/default-avatar.png'. If that prop is undefined, it will use our default string. This is clean, readable, and happens right at the top of your component. No messy logic inside the function body to check for undefined. This is the pattern you’ll use 95% of the time.
The Readonly Tangent: A Free Performance Hint
When you define your props interface, you can (and arguably should) make it readonly. Why? Because props are immutable. You should never change them inside the component. Making them readonly is like giving yourself a seatbelt; it prevents you from making a very silly mistake and accidentally mutating a prop, which is a big React no-no.
interface ButtonProps {
readonly label: string;
readonly onClick: () => void;
}
It’s a small addition that makes your intent crystal clear and keeps you out of trouble.
The Edge Case: When Defaults Live Elsewhere
Sometimes, the default value isn’t a simple primitive. Maybe it’s an expensive function call or an object you don’t want to re-create on every render. In this case, you handle the default inside the component body, but you have to be a bit more explicit.
function ExpensiveComponent({ heavyConfig }: { heavyConfig?: HeavyConfigType }) {
// Use a default only if it's not provided
const config = heavyConfig ?? getDefaultHeavyConfig();
// ... rest of the component
}
Here, we use the nullish coalescing operator (??) to only call getDefaultHeavyConfig() if heavyConfig is actually undefined or null. Placing the default here, instead of in the destructuring, ensures we don’t incur that cost unnecessarily. It’s a minor optimization, but it’s good to know the pattern exists.
The bottom line is this: typing your props isn’t just busywork. It’s the primary way you document your component’s API for both your future self and other developers. It turns runtime errors into compile-time warnings, and the combination of optional props and default destructuring values makes your components robust and flexible without any of the guesswork.