TypeScript in React: A Practical Guide to Better Code

Pawan GangwaniPawan Gangwani
5 min read

If you've been working with React and want to level up your development experience, TypeScript is your next best friend. In this guide, we'll explore how to effectively use TypeScript in React projects, from basic setup to advanced patterns.

Why TypeScript in React?

Before diving in, let's understand why TypeScript makes sense for React projects:

  1. ✨ Better Developer Experience

    • Autocomplete and IntelliSense

    • Catch errors before runtime

    • Better refactoring capabilities

  2. πŸ›‘οΈ Type Safety

    • Prevent common prop errors

    • Ensure correct data structures

    • Validate function parameters and returns

  3. πŸ“š Self-Documenting Code

    • Props requirements are clear

    • Function signatures are explicit

    • Component API is well-defined

Getting Started

Setting Up a New Project

# Create a new React project with TypeScript
npx create-react-app my-app --template typescript

# Or add TypeScript to existing project
npm install --save typescript @types/node @types/react @types/react-dom @types/jest

Basic TypeScript Concepts in React

1. Component Props

// Basic props interface
interface ButtonProps {
  text: string;
  onClick: () => void;
  disabled?: boolean; // Optional prop
}

// Function component with typed props
const Button: React.FC<ButtonProps> = ({ text, onClick, disabled = false }) => {
  return (
    <button 
      onClick={onClick}
      disabled={disabled}
    >
      {text}
    </button>
  );
};

// Usage
<Button 
  text="Click me" 
  onClick={() => console.log('clicked')}
/>

2. State with TypeScript

interface User {
  id: number;
  name: string;
  email: string;
}

function UserProfile() {
  // Type inference works here
  const [loading, setLoading] = useState(false);

  // Explicit type for complex objects
  const [user, setUser] = useState<User | null>(null);

  return (
    <div>
      {loading ? (
        <div>Loading...</div>
      ) : user ? (
        <div>
          <h2>{user.name}</h2>
          <p>{user.email}</p>
        </div>
      ) : null}
    </div>
  );
}

3. Event Handling

interface FormState {
  email: string;
  password: string;
}

function LoginForm() {
  const [form, setForm] = useState<FormState>({
    email: '',
    password: ''
  });

  // Typed event handler
  const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
    const { name, value } = e.target;
    setForm(prev => ({
      ...prev,
      [name]: value
    }));
  };

  // Form submission handler
  const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
    e.preventDefault();
    // Type-safe access to form values
    console.log(form.email, form.password);
  };

  return (
    <form onSubmit={handleSubmit}>
      <input
        type="email"
        name="email"
        value={form.email}
        onChange={handleChange}
      />
      <input
        type="password"
        name="password"
        value={form.password}
        onChange={handleChange}
      />
      <button type="submit">Login</button>
    </form>
  );
}

Advanced Patterns

1. Generic Components

interface ListProps<T> {
  items: T[];
  renderItem: (item: T) => React.ReactNode;
}

function List<T>({ items, renderItem }: ListProps<T>) {
  return (
    <ul>
      {items.map((item, index) => (
        <li key={index}>{renderItem(item)}</li>
      ))}
    </ul>
  );
}

// Usage
interface User {
  id: number;
  name: string;
}

const users: User[] = [
  { id: 1, name: 'John' },
  { id: 2, name: 'Jane' }
];

<List 
  items={users}
  renderItem={(user) => user.name}
/>

2. Type Guards

interface AdminUser {
  type: 'admin';
  name: string;
  privileges: string[];
}

interface RegularUser {
  type: 'regular';
  name: string;
}

type User = AdminUser | RegularUser;

// Type guard function
function isAdmin(user: User): user is AdminUser {
  return user.type === 'admin';
}

function UserPrivileges({ user }: { user: User }) {
  if (isAdmin(user)) {
    // TypeScript knows user is AdminUser here
    return <div>Privileges: {user.privileges.join(', ')}</div>;
  }

  // TypeScript knows user is RegularUser here
  return <div>Regular user: {user.name}</div>;
}

3. Context with TypeScript

interface ThemeContextType {
  theme: 'light' | 'dark';
  toggleTheme: () => void;
}

const ThemeContext = React.createContext<ThemeContextType | undefined>(undefined);

export function ThemeProvider({ children }: { children: React.ReactNode }) {
  const [theme, setTheme] = useState<'light' | 'dark'>('light');

  const toggleTheme = () => {
    setTheme(prev => prev === 'light' ? 'dark' : 'light');
  };

  return (
    <ThemeContext.Provider value={{ theme, toggleTheme }}>
      {children}
    </ThemeContext.Provider>
  );
}

// Custom hook for using theme
export function useTheme() {
  const context = useContext(ThemeContext);
  if (context === undefined) {
    throw new Error('useTheme must be used within a ThemeProvider');
  }
  return context;
}

Project Organization

Here's a recommended way to organize TypeScript files in a React project:

src/
β”œβ”€β”€ types/
β”‚   β”œβ”€β”€ user.ts
β”‚   β”œβ”€β”€ api.ts
β”‚   └── common.ts
β”œβ”€β”€ components/
β”‚   β”œβ”€β”€ Button/
β”‚   β”‚   β”œβ”€β”€ Button.tsx
β”‚   β”‚   β”œβ”€β”€ Button.types.ts
β”‚   β”‚   └── index.ts
β”‚   └── Card/
β”‚       β”œβ”€β”€ Card.tsx
β”‚       β”œβ”€β”€ Card.types.ts
β”‚       └── index.ts
β”œβ”€β”€ hooks/
β”‚   β”œβ”€β”€ useUser.ts
β”‚   └── useApi.ts
└── utils/
    └── typeGuards.ts

Type Definitions Example

// types/user.ts
export interface User {
  id: number;
  name: string;
  email: string;
  role: 'admin' | 'user';
}

// types/api.ts
export interface ApiResponse<T> {
  data: T;
  status: number;
  message: string;
}

// components/Button/Button.types.ts
export interface ButtonProps {
  variant: 'primary' | 'secondary';
  size: 'small' | 'medium' | 'large';
  onClick: () => void;
  children: React.ReactNode;
}

Best Practices

  1. Define Props Interface Separately
// Bad
const Component = ({ name, age }: { name: string; age: number }) => {...}

// Good
interface ComponentProps {
  name: string;
  age: number;
}

const Component: React.FC<ComponentProps> = ({ name, age }) => {...}
  1. Use Type Inference When Possible
// Let TypeScript infer simple state types
const [isOpen, setIsOpen] = useState(false);

// Explicitly type complex states
const [user, setUser] = useState<User | null>(null);
  1. Export Types for Reuse
// types/common.ts
export type Status = 'idle' | 'loading' | 'success' | 'error';
export type Theme = 'light' | 'dark';

// Use throughout your app
import { Status, Theme } from '../types/common';
  1. Use Enum for Constants
enum HttpStatus {
  OK = 200,
  CREATED = 201,
  BAD_REQUEST = 400,
  UNAUTHORIZED = 401
}

function handleResponse(status: HttpStatus) {
  if (status === HttpStatus.OK) {
    // Handle success
  }
}

TypeScript Configuration

A recommended tsconfig.json for React projects:

{
  "compilerOptions": {
    "target": "es5",
    "lib": ["dom", "dom.iterable", "esnext"],
    "allowJs": true,
    "skipLibCheck": true,
    "esModuleInterop": true,
    "allowSyntheticDefaultImports": true,
    "strict": true,
    "forceConsistentCasingInFileNames": true,
    "noFallthroughCasesInSwitch": true,
    "module": "esnext",
    "moduleResolution": "node",
    "resolveJsonModule": true,
    "isolatedModules": true,
    "noEmit": true,
    "jsx": "react-jsx",
    "baseUrl": "src"
  },
  "include": ["src"]
}

Conclusion

TypeScript adds a powerful layer of type safety and developer experience to React projects. By following these patterns and best practices, you can:

  • Write more maintainable code

  • Catch errors early in development

  • Improve team collaboration

  • Create self-documenting components

  • Enable better tooling support

Remember, TypeScript is not about making your code more complicatedβ€”it's about making it more predictable and maintainable. Start with basic type annotations and gradually move to more advanced patterns as your comfort level increases.

Happy coding! πŸš€


If you found this guide helpful, don't forget to like and share! Have questions about TypeScript in React? Drop them in the comments below!

0
Subscribe to my newsletter

Read articles from Pawan Gangwani directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Pawan Gangwani
Pawan Gangwani

I’m Pawan Gangwani, a passionate Full Stack Developer with over 12 years of experience in web development. Currently serving as a Lead Software Engineer at Lowes India, I specialize in modern web applications, particularly in React and performance optimization. I’m dedicated to best practices in coding, testing, and Agile methodologies. Outside of work, I enjoy table tennis, exploring new cuisines, and spending quality time with my family.