Steve Kinney

Migrating from PropTypes to TypeScript

React’s PropTypes were great for their time—they gave us runtime prop validation when JavaScript didn’t have much in the way of static analysis. But now that TypeScript has become the de facto standard for React development, you can get better type safety, earlier error detection, and superior developer experience by migrating from PropTypes to TypeScript interfaces and types.

This migration isn’t just about removing a dependency (though that’s nice). It’s about moving validation from runtime to compile-time, where you can catch issues before they reach your users. Your editor gets smarter, your builds get faster, and your props become self-documenting.

Why Make the Switch?

Before we dive into the mechanics, let’s quickly review why this migration is worth your time:

  • Compile-time vs Runtime Validation: PropTypes only catch errors when your component actually renders with invalid props. TypeScript catches these issues during development, in your editor, before you even run your code.
  • Better Developer Experience: Your editor can autocomplete props, show you what’s expected, and underline issues in real-time. No more hunting through console warnings.
  • Performance Benefits: PropTypes add runtime overhead, especially in development mode where they’re most active. TypeScript validation happens at compile time and produces zero runtime cost.
  • Self-Documenting Code: TypeScript interfaces serve as both validation and documentation, making your component APIs clearer to other developers (including future you).

The Basic Translation

Let’s start with a simple example. Here’s a component using PropTypes:

// ❌ Old `PropTypes` approach
import React from 'react';
import `PropTypes` from 'prop-types';

const UserCard = ({ name, email, age, isActive }) => {
  return (
    <div className={`user-card ${isActive ? 'active' : ''}`}>
      <h2>{name}</h2>
      <p>{email}</p>
      <span>Age: {age}</span>
    </div>
  );
};

UserCard.propTypes = {
  name: PropTypes.string.isRequired,
  email: PropTypes.string.isRequired,
  age: PropTypes.number,
  isActive: PropTypes.bool,
};

UserCard.defaultProps = {
  age: 0,
  isActive: false,
};

Here’s the TypeScript equivalent:

// ✅ TypeScript approach
import React from 'react';

interface UserCardProps {
  name: string;
  email: string;
  age?: number;
  isActive?: boolean;
}

const UserCard = ({ name, email, age = 0, isActive = false }: UserCardProps) => {
  return (
    <div className={`user-card ${isActive ? 'active' : ''}`}>
      <h2>{name}</h2>
      <p>{email}</p>
      <span>Age: {age}</span>
    </div>
  );
};

Notice how much cleaner this is? We’ve eliminated the separate PropTypes definition and moved default values directly into the parameter destructuring. The ? syntax indicates optional props, making the interface more readable than PropTypes ever was.

Handling Complex Types

PropTypes had some limitations when it came to complex data structures. TypeScript shines here:

// ❌ `PropTypes` approach - limited expressiveness
const ProductList = ({ products, onProductClick, filters }) => {
  // Component implementation...
};

ProductList.propTypes = {
  products: PropTypes.arrayOf(
    PropTypes.shape({
      id: PropTypes.number.isRequired,
      name: PropTypes.string.isRequired,
      price: PropTypes.number.isRequired,
      category: PropTypes.string,
    }),
  ).isRequired,
  onProductClick: PropTypes.func.isRequired,
  filters: PropTypes.object, // 😬 Not very specific
};
// ✅ TypeScript approach - precise and expressive
interface Product {
  id: number;
  name: string;
  price: number;
  category?: string;
}

interface ProductFilters {
  minPrice?: number;
  maxPrice?: number;
  categories?: string[];
  inStock?: boolean;
}

interface ProductListProps {
  products: Product[];
  onProductClick: (product: Product) => void;
  filters?: ProductFilters;
}

const ProductList = ({ products, onProductClick, filters = {} }: ProductListProps) => {
  // Component implementation...
};

Look at that onProductClick type! With PropTypes, we could only verify it was a function. TypeScript tells us exactly what parameters it expects and what it returns. Your IDE can now autocomplete the product parameter when you’re implementing the click handler.

Migrating Union Types and Enums

PropTypes had PropTypes.oneOf() for limited value sets. TypeScript’s union types are more powerful:

// ❌ `PropTypes` approach
const Button = ({ variant, size, disabled, onClick, children }) => {
  // Implementation...
};

Button.propTypes = {
  variant: PropTypes.oneOf(['primary', 'secondary', 'danger']),
  size: PropTypes.oneOf(['small', 'medium', 'large']),
  disabled: PropTypes.bool,
  onClick: PropTypes.func.isRequired,
  children: PropTypes.node.isRequired,
};
// ✅ TypeScript approach with union types
type ButtonVariant = 'primary' | 'secondary' | 'danger';
type ButtonSize = 'small' | 'medium' | 'large';

interface ButtonProps {
  variant?: ButtonVariant;
  size?: ButtonSize;
  disabled?: boolean;
  onClick: () => void;
  children: React.ReactNode;
}

const Button = ({
  variant = 'primary',
  size = 'medium',
  disabled = false,
  onClick,
  children,
}: ButtonProps) => {
  // Implementation...
};

Pro tip: Define your union types separately so you can reuse them across components. If you have multiple button-like components, they can all share the same ButtonVariant type.

Dealing with Polymorphic Components

One area where PropTypes really struggled was with polymorphic components—components that can render as different HTML elements. TypeScript handles this elegantly:

// ✅ Polymorphic component with TypeScript
interface HeadingProps<T extends React.ElementType = 'h1'> {
  as?: T;
  level?: 1 | 2 | 3 | 4 | 5 | 6;
  children: React.ReactNode;
}

const Heading = <T extends React.ElementType = 'h1'>({
  as,
  level = 1,
  children,
  ...props
}: HeadingProps<T> & Omit<React.ComponentPropsWithoutRef<T>, keyof HeadingProps>) => {
  const Component = as || (`h${level}` as React.ElementType);

  return <Component {...props}>{children}</Component>;
};

// Usage - TypeScript knows about div-specific props!
<Heading as="div" className="custom-heading" onClick={handleClick}>
  This is a heading
</Heading>;

This might look complex, but it gives you incredible type safety. When you use as="div", TypeScript knows you can use onClick, className, and other div-specific props. With PropTypes, this kind of conditional typing was impossible.

Migration Strategy

Don’t try to migrate everything at once. Here’s a practical approach:

Step 1: Start with Leaf Components

Begin with components that don’t have children components. These are typically easier to migrate and let you build confidence:

// Start here - simple, no dependencies on other components
const LoadingSpinner = ({ size = 'medium' }: { size?: 'small' | 'medium' | 'large' }) => {
  return <div className={`spinner spinner--${size}`} />;
};

Step 2: Create Shared Types

As you migrate, you’ll start to notice patterns. Extract common types:

// types/common.ts
export interface User {
  id: string;
  name: string;
  email: string;
  avatar?: string;
}

export interface ApiError {
  message: string;
  code: number;
  details?: Record<string, unknown>;
}

export type LoadingState = 'idle' | 'loading' | 'success' | 'error';

Step 3: Migrate Parent Components

Once your leaf components are typed, work your way up the component tree. The TypeScript compiler will help you find any mismatches.

Step 4: Remove PropTypes

Only after you’ve fully migrated a component should you remove its PropTypes. This lets you verify the TypeScript types are working correctly before losing the runtime validation safety net.

# Remove `PropTypes` dependency when you're done
npm uninstall prop-types
# or
yarn remove prop-types

Common Gotchas and Solutions

Children Props

PropTypes used PropTypes.node for anything renderable. TypeScript is more specific:

// ❌ Too restrictive
interface Props {
  children: JSX.Element; // Only accepts single React elements
}

// ✅ Flexible like PropTypes.node
interface Props {
  children: React.ReactNode; // Accepts strings, numbers, elements, arrays, etc.
}

// ✅ Even more specific when you know what you expect
interface Props {
  children: string; // Only accepts strings
}

Ref Forwarding

If you’re using React.forwardRef, the typing is slightly different:

// ✅ Proper ref forwarding with TypeScript
interface InputProps {
  placeholder?: string;
  disabled?: boolean;
}

const Input = React.forwardRef<HTMLInputElement, InputProps>(
  ({ placeholder, disabled, ...props }, ref) => {
    return <input ref={ref} placeholder={placeholder} disabled={disabled} {...props} />;
  },
);

Input.displayName = 'Input';

Event Handlers

PropTypes couldn’t tell you much about event handlers. TypeScript can be very specific:

interface FormProps {
  onSubmit: (event: React.FormEvent<HTMLFormElement>) => void;
  onChange: (event: React.ChangeEvent<HTMLInputElement>) => void;
  onFocus: (event: React.FocusEvent<HTMLInputElement>) => void;
}

Your IDE will now autocomplete event.target, event.preventDefault(), and show you exactly what properties are available.

Validating Props at Runtime

“But wait,” you might ask, “what about runtime validation? What if my API returns malformed data?”

Great question! TypeScript handles compile-time validation, but you still need runtime validation for external data. This is where libraries like Zod come in handy:

import { z } from 'zod';

const UserSchema = z.object({
  id: z.string(),
  name: z.string(),
  email: z.string().email(),
  age: z.number().optional(),
});

type User = z.infer<typeof UserSchema>;

// Validate API responses
const fetchUser = async (id: string): Promise<User> => {
  const response = await fetch(`/api/users/${id}`);
  const data = await response.json();

  // This will throw if the data doesn't match our schema
  return UserSchema.parse(data);
};

This gives you the best of both worlds: compile-time safety from TypeScript and runtime validation where you actually need it (at the boundaries of your application).

Performance Considerations

One immediate benefit you’ll notice after removing PropTypes is faster development builds and smaller bundle sizes. PropTypes validation code gets stripped out in production builds, but it still needs to be processed during development.

Here’s what you’re eliminating:

// This entire block disappears
UserCard.propTypes = {
  name: PropTypes.string.isRequired,
  email: PropTypes.string.isRequired,
  age: PropTypes.number,
  isActive: PropTypes.bool,
};

For large applications with many components, this can add up to meaningful bundle size reduction and faster hot module replacement during development.

When You Might Keep PropTypes

There are a few scenarios where you might want to keep PropTypes around temporarily:

  • Third-party Integration: If you’re building components that will be consumed by JavaScript (non-TypeScript) projects, PropTypes can provide runtime safety for those consumers.
  • Gradual Migration: During a large migration, having both TypeScript types and PropTypes can provide redundant safety while you’re building confidence in your new types.
  • Runtime Configuration: If your components receive props from configuration files or external systems at runtime, PropTypes can catch misconfigurations that TypeScript can’t see.

But for most React applications, these scenarios are rare, and the benefits of pure TypeScript far outweigh keeping PropTypes around.

Wrapping Up

Migrating from PropTypes to TypeScript isn’t just about following trends—it’s about fundamentally improving how you build React applications. You get earlier error detection, better tooling support, self-documenting code, and improved performance.

The migration might feel like extra work upfront, but every component you convert becomes more robust, easier to maintain, and more pleasant to work with. Your future self (and your teammates) will thank you for making the investment.

Start small, migrate component by component, and before you know it, you’ll have a fully typed React application that’s more reliable and easier to develop than ever before.

Last modified on .