Adopting TypeScript in a Large React Codebase - A Practical Guide

typescriptreactmigrationjavascriptcoding

background

Migrating a large React codebase from JavaScript to TypeScript can seem daunting. At Dentira, we successfully completed this migration while maintaining team productivity and improving code quality. Here's what we learned.

Why TypeScript?

Before starting the migration, we identified several benefits:

  • Better Developer Experience: IDE autocomplete and IntelliSense
  • Catch Bugs Early: Type checking catches errors at compile time
  • Improved Refactoring: Safer refactoring with type safety
  • Better Documentation: Types serve as inline documentation

Migration Strategy

1. Incremental Adoption

TypeScript allows gradual migration. You don't need to convert everything at once:

// tsconfig.json
{
  "compilerOptions": {
    "allowJs": true,  // Allow JavaScript files
    "checkJs": false  // Don't type-check JS files initially
  }
}

2. Start with New Code

Our strategy was simple:

  • All new files must be TypeScript
  • Gradually migrate existing files when touched
  • Prioritize critical paths and shared utilities

3. Define Types Incrementally

Start with any if needed, but gradually refine types:

// Start here
function processOrder(order: any) {
  // ...
}

// Refine over time
interface Order {
  id: string;
  items: OrderItem[];
  total: number;
}

function processOrder(order: Order) {
  // ...
}

Common Challenges

1. Third-Party Library Types

Some libraries don't have type definitions:

// Install types if available
npm install --save-dev @types/library-name

// Or create your own definitions
declare module 'library-name' {
  export function doSomething(param: string): void;
}

2. React Component Props

Properly typing React components:

interface ProductCardProps {
  product: Product;
  onAddToCart: (id: string) => void;
  className?: string;
}

const ProductCard: React.FC<ProductCardProps> = ({
  product,
  onAddToCart,
  className
}) => {
  // Component implementation
};

3. Event Handlers

Typing event handlers correctly:

const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
  setValue(e.target.value);
};

const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
  e.preventDefault();
  // Submit logic
};

Tools That Helped

1. TypeScript ESLint

Catch type errors and enforce best practices:

{
  "extends": [
    "plugin:@typescript-eslint/recommended"
  ]
}

2. Type Coverage Tool

Track migration progress:

npx type-coverage

3. ts-migrate (Optional)

For large migrations, tools like ts-migrate can automate some of the work, though we found manual migration gave better results.

Best Practices

1. Avoid any

Use unknown or proper types instead:

// Bad
function process(data: any) { }

// Better
function process(data: unknown) {
  if (isValidData(data)) {
    // Now TypeScript knows the type
  }
}

2. Use Type Utilities

Leverage TypeScript's utility types:

type PartialOrder = Partial<Order>;
type OrderId = Pick<Order, 'id'>;
type OrderWithoutTotal = Omit<Order, 'total'>;

3. Generics for Reusability

Use generics to create reusable, type-safe utilities:

function createApiCall<T>(endpoint: string): Promise<T> {
  return fetch(endpoint).then(res => res.json());
}

const orders = await createApiCall<Order[]>('/api/orders');

Results

After completing the migration:

  • Reduced Runtime Errors: Type checking caught many potential bugs
  • Improved Developer Productivity: Better autocomplete and refactoring
  • Better Onboarding: New team members could understand code faster
  • Increased Confidence: Safer deployments with compile-time checks

Lessons Learned

  1. Start Small: Don't try to migrate everything at once
  2. Get Team Buy-in: Ensure everyone understands the benefits
  3. Invest in Training: TypeScript has a learning curve
  4. Be Patient: The migration takes time, but the benefits compound

The migration to TypeScript significantly improved our codebase quality at Dentira. While it required upfront investment, the long-term benefits in maintainability, developer experience, and bug prevention made it worthwhile.

If you're considering a TypeScript migration, start with new code and gradually work backwards. The incremental approach reduces risk and allows your team to learn as you go.