PropTypes and Default Props — Documenting Component Contracts

As your MERN Blog grows and other developers (or your future self) start using the components you have built, it becomes valuable to document what props each component expects and what types they should be. PropTypes is React’s built-in runtime type checking system — it validates prop types in development mode and logs a console warning when the wrong type is passed. This lesson covers the complete PropTypes API, how to declare default props cleanly, and when to consider upgrading to TypeScript for compile-time type safety.

Why PropTypes Matter

Without PropTypes With PropTypes
Wrong prop type causes silent bug Console warning immediately identifies the problem
Other developers must read source to understand the API PropTypes serve as live documentation
Missing required prop discovered at runtime Missing required prop warned in development
API changes silently break call sites Call sites see warnings when a prop type changes
Note: PropTypes only run in development mode. In production builds, React strips out all PropTypes checks for performance. They are a development tool — not a replacement for proper input validation in your Express API or for handling null/undefined defensively in your React code.
Tip: For TypeScript users, PropTypes are unnecessary — TypeScript provides stronger, compile-time type checking. For JavaScript + Vite projects (like this series), PropTypes provide a meaningful safety net. If you find yourself writing large, complex PropTypes objects repeatedly, that is a signal the project has grown to where migrating to TypeScript would pay off.
Warning: PropTypes are only checked for the component they are defined on. If you pass an object prop and define PropTypes.object, React will not validate the object’s internal structure. Use PropTypes.shape({}) to validate object props, not just that the prop is an object.

Installation and Import

cd client
npm install prop-types
import PropTypes from 'prop-types';

Core PropTypes Validators

import PropTypes from 'prop-types';

function PostCard({ post, featured, onDelete, variant }) {
  return <article><h2>{post.title}</h2></article>;
}

PostCard.propTypes = {
  // Primitive types
  featured:  PropTypes.bool,
  viewCount: PropTypes.number,
  title:     PropTypes.string,

  // Required
  onDelete: PropTypes.func.isRequired,

  // Object with known shape
  post: PropTypes.shape({
    _id:       PropTypes.string.isRequired,
    title:     PropTypes.string.isRequired,
    excerpt:   PropTypes.string,
    viewCount: PropTypes.number,
    published: PropTypes.bool,
    tags:      PropTypes.arrayOf(PropTypes.string),
    author: PropTypes.shape({
      _id:    PropTypes.string,
      name:   PropTypes.string.isRequired,
      avatar: PropTypes.string,
    }),
  }).isRequired,

  // Enum — one of a fixed set
  variant: PropTypes.oneOf(['standard', 'featured', 'compact']),

  // Array of a specific type
  tags: PropTypes.arrayOf(PropTypes.string),

  // Any React-renderable content
  children: PropTypes.node,

  // One of multiple types
  id: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
};

Default Props — Modern Approach

// Preferred: default values directly in destructuring
function PostCard({
  post,
  featured  = false,
  variant   = 'standard',
  onDelete  = () => {},
  tags      = [],
}) {
  // Defaults are self-documenting here in the signature
}

// Classic approach (still valid, may eventually be deprecated for function components)
PostCard.defaultProps = {
  featured: false,
  variant:  'standard',
  onDelete: () => {},
  tags:     [],
};

PropTypes for MERN Blog Components

Avatar.propTypes = {
  src:  PropTypes.string,
  name: PropTypes.string.isRequired,
  size: PropTypes.number,
};

Spinner.propTypes = {
  size:    PropTypes.oneOf(['small', 'medium', 'large']),
  message: PropTypes.string,
};

Badge.propTypes = {
  label:   PropTypes.string.isRequired,
  variant: PropTypes.oneOf(['default', 'tag', 'success', 'warning', 'error']),
};

ErrorMessage.propTypes = {
  message: PropTypes.string.isRequired,
  onRetry: PropTypes.func,
};

Common Mistakes

Mistake 1 — Using PropTypes.object instead of PropTypes.shape

❌ Wrong — only confirms it is an object, not what is inside:

PostCard.propTypes = { post: PropTypes.object }; // any object passes

✅ Correct — validate the structure:

PostCard.propTypes = {
  post: PropTypes.shape({ title: PropTypes.string.isRequired }).isRequired,
};

Mistake 2 — Forgetting .isRequired on essential props

❌ Wrong — no warning when a critical prop is omitted:

PostCard.propTypes = { post: PropTypes.shape({ ... }) }; // optional by default

✅ Correct — add .isRequired:

PostCard.propTypes = { post: PropTypes.shape({ ... }).isRequired }; // ✓

Mistake 3 — Writing PropTypes.propTypes after default export on anonymous function

❌ Wrong — PostCard is not in scope after an anonymous default export:

export default function PostCard({ post }) { ... }
PostCard.propTypes = { ... }; // ReferenceError in some bundler setups

✅ Correct — name the function, add PropTypes, then export:

function PostCard({ post }) { ... }
PostCard.propTypes = { post: PropTypes.shape({...}).isRequired };
export default PostCard; // ✓

Quick Reference

Validator Use For
PropTypes.string Text props
PropTypes.number Numeric props
PropTypes.bool Boolean flags
PropTypes.func Callback props
PropTypes.arrayOf(T) Typed array
PropTypes.shape({}) Object with known structure
PropTypes.oneOf([]) Enum
PropTypes.node Any renderable content
.isRequired Prop must be provided

🧠 Test Yourself

You define post: PropTypes.object.isRequired. A post without a title crashes the component. Why didn’t PropTypes warn about the missing title?