Prop Types and Default Values

Difficulty: Beginner

When building React components, it is important to document and validate the props a component expects. PropTypes is a runtime type-checking library that was originally part of React and is now a separate package (`prop-types`). It lets you define the expected types for each prop, and React will log warnings in development when a prop of the wrong type is passed. While TypeScript has largely replaced PropTypes for type safety, understanding PropTypes is still valuable for many codebases.

PropTypes are defined as a static property on the component function. You specify each prop name and its expected type using validators like `PropTypes.string`, `PropTypes.number`, `PropTypes.bool`, `PropTypes.func`, `PropTypes.array`, `PropTypes.object`, and more. Appending `.isRequired` makes the prop mandatory - React will warn if it is not provided. PropTypes only run in development mode and are stripped in production builds for performance.

Default prop values ensure your component works gracefully when optional props are not provided. There are two ways to set defaults. The older pattern uses a `defaultProps` static property on the component. The modern and preferred approach is to use JavaScript default parameter syntax directly in the function signature: `function Button({ variant = 'primary', size = 'medium' })`. This is cleaner, works with TypeScript, and is the approach the React team recommends.

Destructuring props is a pattern where you extract specific properties from the props object directly in the function parameters. Instead of writing `function Button(props)` and accessing `props.variant`, you write `function Button({ variant, size, onClick })`. This makes the component's API visible at a glance and allows you to set default values inline. Destructuring is used in virtually all modern React codebases.

For TypeScript projects, you define prop types using interfaces or type aliases instead of PropTypes. This provides compile-time type checking, better IDE support, and does not add any runtime overhead. The pattern is: `interface ButtonProps { variant?: 'primary' | 'secondary'; onClick: () => void; }` followed by `function Button({ variant = 'primary', onClick }: ButtonProps)`. This is considered the best practice in the React ecosystem today.

Code examples

Destructuring Props with Defaults

// Modern pattern: destructure props with default values
function Button({ label, variant = 'primary', size = 'medium', disabled = false }) {
  return {
    label,
    variant,
    size,
    disabled
  };
}

// All defaults applied
const btn1 = Button({ label: 'Click Me' });
console.log(btn1.label + ' | ' + btn1.variant + ' | ' + btn1.size + ' | ' + btn1.disabled);

// Override some defaults
const btn2 = Button({ label: 'Delete', variant: 'danger', size: 'large' });
console.log(btn2.label + ' | ' + btn2.variant + ' | ' + btn2.size + ' | ' + btn2.disabled);

Destructuring in the function signature makes the component API visible at a glance. Default values ensure missing props don't cause errors.

PropTypes Validation (Conceptual)

// Simulating PropTypes validation
function validateProps(props, propTypes) {
  const errors = [];
  for (const [key, validator] of Object.entries(propTypes)) {
    const value = props[key];
    const expectedType = validator.type;
    const isRequired = validator.required;

    if (value === undefined && isRequired) {
      errors.push(key + ' is required');
    } else if (value !== undefined && typeof value !== expectedType) {
      errors.push(key + ' expected ' + expectedType + ', got ' + typeof value);
    }
  }
  return errors;
}

const propTypes = {
  name: { type: 'string', required: true },
  age: { type: 'number', required: false },
  onSave: { type: 'function', required: true }
};

const errors1 = validateProps({ name: 'Alice', onSave: () => {} }, propTypes);
console.log('Valid: ' + (errors1.length === 0));

const errors2 = validateProps({ name: 42, age: 'old' }, propTypes);
errors2.forEach(e => console.log('Error: ' + e));

PropTypes validate props at runtime during development. They catch type mismatches and missing required props, logging warnings to the console.

Rest and Spread with Props

// Destructure known props, collect the rest
function Input({ label, type = 'text', ...restProps }) {
  return {
    label,
    inputProps: { type, ...restProps }
  };
}

const field = Input({
  label: 'Email',
  type: 'email',
  placeholder: 'Enter email',
  required: true,
  autoComplete: 'email'
});

console.log('Label: ' + field.label);
console.log('Type: ' + field.inputProps.type);
console.log('Placeholder: ' + field.inputProps.placeholder);
console.log('Required: ' + field.inputProps.required);

The rest operator (...restProps) collects all props not explicitly destructured. This is useful for forwarding unknown props to child elements.

Key points

Concepts covered

PropTypes, default props, destructuring props, type checking, optional props