Falytom logoFALYTOM
Games
ToolsUI ComponentsServicesProductsAbout
LoginSign Up
menu
  1. Home
  2. /Web Development
  3. /Typescript
  4. /Typescript With React
Your Progress0%
0 of 30 completed

TypeScript Topics

Getting Started

  • What is TypeScript?
  • TypeScript vs JavaScript
  • Setting Up TypeScript
  • Your First TypeScript Program

Basic Types

  • Primitive Types
  • Arrays and Tuples
  • Objects and Type Aliases
  • Enums
  • Any, Unknown, and Never

Functions & Interfaces

  • Function Types and Signatures
  • Interfaces
  • Optional and Default Parameters
  • Function Overloading

Advanced Types

  • Union and Intersection Types
  • Type Guards and Narrowing
  • Literal Types
  • Type Assertions and Casting

Generics

  • Introduction to Generics
  • Generic Constraints
  • Utility Types

Classes & OOP

  • Classes and Constructors
  • Access Modifiers
  • Inheritance and Polymorphism
  • Abstract Classes and Interfaces

Practical TypeScript

  • Working with Modules
  • TypeScript with React
  • Async/Await and Promises
  • Working with APIs and JSON
  • Type Declarations and DefinitelyTyped
  • TypeScript Best Practices

TypeScript with React

Typing React components, props, state, and hooks

TypeScript and React are a powerful combination for building type-safe, maintainable web applications. TypeScript provides autocomplete, type checking, and excellent developer experience when working with React components, props, state, and hooks. Let's master TypeScript with React!

Function Components and Props

Function components can be typed by defining a props interface and typing the component parameters.

components/User.tsx
import React from 'react';

// Define props interface
interface UserProps {
  name: string;
  age: number;
  email: string;
  role?: string;  // Optional prop
}

// Method 1: Inline props typing
function User({ name, age, email, role = "user" }: UserProps) {
  return (
    <div className="user-card">
      <h2>{name}</h2>
      <p>Age: {age}</p>
      <p>Email: {email}</p>
      <p>Role: {role}</p>
    </div>
  );
}

// Method 2: Using React.FC (Function Component)
const UserFC: React.FC<UserProps> = ({ name, age, email, role = "user" }) => {
  return (
    <div className="user-card">
      <h2>{name}</h2>
      <p>Age: {age}</p>
      <p>Email: {email}</p>
      <p>Role: {role}</p>
    </div>
  );
};

// Usage
function App() {
  return (
    <div>
      <User 
        name="Yusuf Ibrahim" 
        age={25} 
        email="yusuf@example.com"
        role="admin"
      />
      
      {/* Optional prop can be omitted */}
      <User 
        name="Amina Hassan" 
        age={30} 
        email="amina@example.com"
      />
    </div>
  );
}

export default User;

💡 React.FC vs Regular Function

React.FC automatically includes children type and provides better type inference in some cases. However, modern React developers often prefer regular function components with inline props typing for simplicity. Both approaches work well!

Children Prop

components/Card.tsx
import React from 'react';

// Using React.ReactNode for children
interface CardProps {
  title: string;
  children: React.ReactNode;  // Can be any valid React child
}

function Card({ title, children }: CardProps) {
  return (
    <div className="card">
      <h3>{title}</h3>
      <div className="card-content">
        {children}
      </div>
    </div>
  );
}

// More specific children types
interface ButtonProps {
  children: string;  // Only string children
  onClick: () => void;
}

function Button({ children, onClick }: ButtonProps) {
  return <button onClick={onClick}>{children}</button>;
}

// Multiple children of specific type
interface ListProps {
  children: React.ReactElement<ItemProps>[];  // Array of Item components
}

interface ItemProps {
  text: string;
}

function Item({ text }: ItemProps) {
  return <li>{text}</li>;
}

function List({ children }: ListProps) {
  return <ul>{children}</ul>;
}

// Usage
function App() {
  return (
    <div>
      <Card title="User Information">
        <p>Name: Yusuf Ibrahim</p>
        <p>Email: yusuf@example.com</p>
        <Button onClick={() => console.log("Clicked")}>
          View Profile
        </Button>
      </Card>
      
      <List>
        <Item text="First item" />
        <Item text="Second item" />
        <Item text="Third item" />
      </List>
    </div>
  );
}

export default Card;

Event Handlers

components/Form.tsx
import React, { useState } from 'react';

interface FormProps {
  onSubmit: (data: FormData) => void;
}

interface FormData {
  username: string;
  email: string;
  age: number;
}

function Form({ onSubmit }: FormProps) {
  const [username, setUsername] = useState<string>("");
  const [email, setEmail] = useState<string>("");
  const [age, setAge] = useState<number>(0);
  
  // Click event
  const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
    e.preventDefault();
    onSubmit({ username, email, age });
  };
  
  // Input change event
  const handleUsernameChange = (e: React.ChangeEvent<HTMLInputElement>) => {
    setUsername(e.target.value);
  };
  
  // Select change event
  const handleAgeChange = (e: React.ChangeEvent<HTMLSelectElement>) => {
    setAge(Number(e.target.value));
  };
  
  // Mouse event
  const handleMouseEnter = (e: React.MouseEvent<HTMLDivElement>) => {
    console.log("Mouse entered at:", e.clientX, e.clientY);
  };
  
  // Keyboard event
  const handleKeyPress = (e: React.KeyboardEvent<HTMLInputElement>) => {
    if (e.key === "Enter") {
      console.log("Enter pressed");
    }
  };
  
  return (
    <div onMouseEnter={handleMouseEnter}>
      <form onSubmit={handleSubmit}>
        <input
          type="text"
          value={username}
          onChange={handleUsernameChange}
          onKeyPress={handleKeyPress}
          placeholder="Username"
        />
        
        <input
          type="email"
          value={email}
          onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
            setEmail(e.target.value);
          }}
          placeholder="Email"
        />
        
        <select value={age} onChange={handleAgeChange}>
          <option value={0}>Select age</option>
          <option value={18}>18</option>
          <option value={25}>25</option>
          <option value={30}>30</option>
        </select>
        
        <button type="submit">Submit</button>
      </form>
    </div>
  );
}

// Usage
function App() {
  const handleFormSubmit = (data: FormData) => {
    console.log("Form submitted:", data);
  };
  
  return <Form onSubmit={handleFormSubmit} />;
}

export default Form;

useState Hook

components/Counter.tsx
import React, { useState } from 'react';

// Type inference from initial value
function Counter() {
  // TypeScript infers count is number
  const [count, setCount] = useState(0);
  
  // TypeScript infers text is string
  const [text, setText] = useState("Hello");
  
  // TypeScript infers isActive is boolean
  const [isActive, setIsActive] = useState(true);
  
  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={() => setCount(count + 1)}>Increment</button>
      <button onClick={() => setCount(count - 1)}>Decrement</button>
    </div>
  );
}

// Explicit type parameter
interface User {
  id: number;
  name: string;
  email: string;
}

function UserProfile() {
  // Explicit type when initial value is null or undefined
  const [user, setUser] = useState<User | null>(null);
  
  // Array type
  const [users, setUsers] = useState<User[]>([]);
  
  // Complex type
  const [settings, setSettings] = useState<{
    theme: "light" | "dark";
    notifications: boolean;
    language: string;
  }>({
    theme: "light",
    notifications: true,
    language: "en"
  });
  
  const loadUser = () => {
    const newUser: User = {
      id: 1,
      name: "Yusuf Ibrahim",
      email: "yusuf@example.com"
    };
    setUser(newUser);
  };
  
  const addUser = (newUser: User) => {
    setUsers(prevUsers => [...prevUsers, newUser]);
  };
  
  const updateSettings = () => {
    setSettings(prev => ({
      ...prev,
      theme: prev.theme === "light" ? "dark" : "light"
    }));
  };
  
  return (
    <div>
      {user ? (
        <div>
          <h2>{user.name}</h2>
          <p>{user.email}</p>
        </div>
      ) : (
        <button onClick={loadUser}>Load User</button>
      )}
      
      <p>Theme: {settings.theme}</p>
      <button onClick={updateSettings}>Toggle Theme</button>
      
      <p>Total Users: {users.length}</p>
    </div>
  );
}

export default Counter;

useEffect Hook

components/DataFetcher.tsx
import React, { useState, useEffect } from 'react';

interface Product {
  id: number;
  name: string;
  price: number;
}

function DataFetcher() {
  const [products, setProducts] = useState<Product[]>([]);
  const [loading, setLoading] = useState<boolean>(true);
  const [error, setError] = useState<string | null>(null);
  
  // Effect with cleanup
  useEffect(() => {
    let isMounted = true;
    
    const fetchProducts = async () => {
      try {
        setLoading(true);
        const response = await fetch('/api/products');
        const data: Product[] = await response.json();
        
        if (isMounted) {
          setProducts(data);
          setError(null);
        }
      } catch (err) {
        if (isMounted) {
          setError((err as Error).message);
        }
      } finally {
        if (isMounted) {
          setLoading(false);
        }
      }
    };
    
    fetchProducts();
    
    // Cleanup function
    return () => {
      isMounted = false;
    };
  }, []);  // Empty dependency array - run once
  
  // Effect with dependencies
  const [searchTerm, setSearchTerm] = useState<string>("");
  const [filteredProducts, setFilteredProducts] = useState<Product[]>([]);
  
  useEffect(() => {
    const filtered = products.filter(product =>
      product.name.toLowerCase().includes(searchTerm.toLowerCase())
    );
    setFilteredProducts(filtered);
  }, [products, searchTerm]);  // Re-run when these change
  
  // Effect with interval cleanup
  const [timer, setTimer] = useState<number>(0);
  
  useEffect(() => {
    const intervalId = setInterval(() => {
      setTimer(prev => prev + 1);
    }, 1000);
    
    // Cleanup interval
    return () => {
      clearInterval(intervalId);
    };
  }, []);
  
  if (loading) return <div>Loading...</div>;
  if (error) return <div>Error: {error}</div>;
  
  return (
    <div>
      <p>Timer: {timer}s</p>
      
      <input
        type="text"
        value={searchTerm}
        onChange={(e) => setSearchTerm(e.target.value)}
        placeholder="Search products..."
      />
      
      <div>
        {filteredProducts.map(product => (
          <div key={product.id}>
            <h3>{product.name}</h3>
            <p>₦{product.price.toLocaleString()}</p>
          </div>
        ))}
      </div>
    </div>
  );
}

export default DataFetcher;

useRef Hook

components/InputFocus.tsx
import React, { useRef, useEffect } from 'react';

function InputFocus() {
  // Ref for DOM element
  const inputRef = useRef<HTMLInputElement>(null);
  
  // Ref for mutable value
  const countRef = useRef<number>(0);
  
  // Ref for timeout
  const timeoutRef = useRef<NodeJS.Timeout | null>(null);
  
  useEffect(() => {
    // Focus input on mount
    if (inputRef.current) {
      inputRef.current.focus();
    }
  }, []);
  
  const handleClick = () => {
    // Access DOM element
    if (inputRef.current) {
      console.log("Input value:", inputRef.current.value);
      inputRef.current.select();
    }
    
    // Update mutable value (doesn't cause re-render)
    countRef.current += 1;
    console.log("Clicks:", countRef.current);
  };
  
  const startTimer = () => {
    if (timeoutRef.current) {
      clearTimeout(timeoutRef.current);
    }
    
    timeoutRef.current = setTimeout(() => {
      console.log("Timer finished");
    }, 3000);
  };
  
  useEffect(() => {
    // Cleanup timeout on unmount
    return () => {
      if (timeoutRef.current) {
        clearTimeout(timeoutRef.current);
      }
    };
  }, []);
  
  return (
    <div>
      <input
        ref={inputRef}
        type="text"
        placeholder="Type here..."
      />
      <button onClick={handleClick}>Focus & Select</button>
      <button onClick={startTimer}>Start Timer</button>
    </div>
  );
}

// Ref forwarding
interface CustomInputProps {
  placeholder?: string;
}

const CustomInput = React.forwardRef<HTMLInputElement, CustomInputProps>(
  ({ placeholder }, ref) => {
    return (
      <input
        ref={ref}
        type="text"
        placeholder={placeholder}
        className="custom-input"
      />
    );
  }
);

CustomInput.displayName = "CustomInput";

function App() {
  const customInputRef = useRef<HTMLInputElement>(null);
  
  const focusCustomInput = () => {
    customInputRef.current?.focus();
  };
  
  return (
    <div>
      <CustomInput ref={customInputRef} placeholder="Custom input" />
      <button onClick={focusCustomInput}>Focus Custom Input</button>
    </div>
  );
}

export default InputFocus;

Custom Hooks

hooks/useLocalStorage.ts
import { useState, useEffect } from 'react';

// Custom hook with generic type
function useLocalStorage<T>(
  key: string,
  initialValue: T
): [T, (value: T | ((prev: T) => T)) => void] {
  // Get initial value from localStorage or use provided initial value
  const [storedValue, setStoredValue] = useState<T>(() => {
    try {
      const item = window.localStorage.getItem(key);
      return item ? JSON.parse(item) : initialValue;
    } catch (error) {
      console.error(`Error reading localStorage key "${key}":`, error);
      return initialValue;
    }
  });
  
  // Update localStorage when value changes
  const setValue = (value: T | ((prev: T) => T)) => {
    try {
      // Handle function updater
      const valueToStore = value instanceof Function ? value(storedValue) : value;
      
      setStoredValue(valueToStore);
      window.localStorage.setItem(key, JSON.stringify(valueToStore));
    } catch (error) {
      console.error(`Error setting localStorage key "${key}":`, error);
    }
  };
  
  return [storedValue, setValue];
}

// Usage
function App() {
  const [name, setName] = useLocalStorage<string>("name", "");
  const [age, setAge] = useLocalStorage<number>("age", 0);
  const [user, setUser] = useLocalStorage<{ id: number; name: string } | null>(
    "user",
    null
  );
  
  return (
    <div>
      <input
        value={name}
        onChange={(e) => setName(e.target.value)}
        placeholder="Name"
      />
      
      <input
        type="number"
        value={age}
        onChange={(e) => setAge(Number(e.target.value))}
        placeholder="Age"
      />
      
      <button
        onClick={() => setUser({ id: 1, name: "Yusuf" })}
      >
        Set User
      </button>
    </div>
  );
}

export default useLocalStorage;
hooks/useFetch.ts
import { useState, useEffect } from 'react';

interface FetchState<T> {
  data: T | null;
  loading: boolean;
  error: string | null;
}

function useFetch<T>(url: string): FetchState<T> {
  const [data, setData] = useState<T | null>(null);
  const [loading, setLoading] = useState<boolean>(true);
  const [error, setError] = useState<string | null>(null);
  
  useEffect(() => {
    let isMounted = true;
    
    const fetchData = async () => {
      try {
        setLoading(true);
        const response = await fetch(url);
        
        if (!response.ok) {
          throw new Error(`HTTP error! status: ${response.status}`);
        }
        
        const result: T = await response.json();
        
        if (isMounted) {
          setData(result);
          setError(null);
        }
      } catch (err) {
        if (isMounted) {
          setError((err as Error).message);
          setData(null);
        }
      } finally {
        if (isMounted) {
          setLoading(false);
        }
      }
    };
    
    fetchData();
    
    return () => {
      isMounted = false;
    };
  }, [url]);
  
  return { data, loading, error };
}

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

function UserList() {
  const { data: users, loading, error } = useFetch<User[]>('/api/users');
  
  if (loading) return <div>Loading...</div>;
  if (error) return <div>Error: {error}</div>;
  if (!users) return <div>No data</div>;
  
  return (
    <div>
      {users.map(user => (
        <div key={user.id}>
          <h3>{user.name}</h3>
          <p>{user.email}</p>
        </div>
      ))}
    </div>
  );
}

export default useFetch;

Practical Examples

Example 1: Todo List with TypeScript

components/TodoList.tsx
import React, { useState } from 'react';

interface Todo {
  id: number;
  text: string;
  completed: boolean;
  createdAt: Date;
}

interface TodoItemProps {
  todo: Todo;
  onToggle: (id: number) => void;
  onDelete: (id: number) => void;
}

function TodoItem({ todo, onToggle, onDelete }: TodoItemProps) {
  return (
    <div className="todo-item">
      <input
        type="checkbox"
        checked={todo.completed}
        onChange={() => onToggle(todo.id)}
      />
      <span
        style={{
          textDecoration: todo.completed ? 'line-through' : 'none'
        }}
      >
        {todo.text}
      </span>
      <button onClick={() => onDelete(todo.id)}>Delete</button>
    </div>
  );
}

function TodoList() {
  const [todos, setTodos] = useState<Todo[]>([]);
  const [inputText, setInputText] = useState<string>("");
  
  const addTodo = (e: React.FormEvent<HTMLFormElement>) => {
    e.preventDefault();
    
    if (!inputText.trim()) return;
    
    const newTodo: Todo = {
      id: Date.now(),
      text: inputText,
      completed: false,
      createdAt: new Date()
    };
    
    setTodos(prevTodos => [...prevTodos, newTodo]);
    setInputText("");
  };
  
  const toggleTodo = (id: number) => {
    setTodos(prevTodos =>
      prevTodos.map(todo =>
        todo.id === id
          ? { ...todo, completed: !todo.completed }
          : todo
      )
    );
  };
  
  const deleteTodo = (id: number) => {
    setTodos(prevTodos => prevTodos.filter(todo => todo.id !== id));
  };
  
  const completedCount = todos.filter(t => t.completed).length;
  const totalCount = todos.length;
  
  return (
    <div className="todo-list">
      <h1>Todo List</h1>
      
      <form onSubmit={addTodo}>
        <input
          type="text"
          value={inputText}
          onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
            setInputText(e.target.value);
          }}
          placeholder="Add a todo..."
        />
        <button type="submit">Add</button>
      </form>
      
      <div className="stats">
        <p>
          Completed: {completedCount} / {totalCount}
        </p>
      </div>
      
      <div className="todos">
        {todos.map(todo => (
          <TodoItem
            key={todo.id}
            todo={todo}
            onToggle={toggleTodo}
            onDelete={deleteTodo}
          />
        ))}
      </div>
    </div>
  );
}

export default TodoList;

Example 2: Form with Validation

components/RegistrationForm.tsx
import React, { useState } from 'react';

interface FormData {
  username: string;
  email: string;
  password: string;
  confirmPassword: string;
}

interface FormErrors {
  username?: string;
  email?: string;
  password?: string;
  confirmPassword?: string;
}

type FormField = keyof FormData;

function RegistrationForm() {
  const [formData, setFormData] = useState<FormData>({
    username: "",
    email: "",
    password: "",
    confirmPassword: ""
  });
  
  const [errors, setErrors] = useState<FormErrors>({});
  const [touched, setTouched] = useState<Record<FormField, boolean>>({
    username: false,
    email: false,
    password: false,
    confirmPassword: false
  });
  
  const validateField = (field: FormField, value: string): string | undefined => {
    switch (field) {
      case "username":
        if (!value) return "Username is required";
        if (value.length < 3) return "Username must be at least 3 characters";
        return undefined;
        
      case "email":
        if (!value) return "Email is required";
        if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value)) {
          return "Invalid email format";
        }
        return undefined;
        
      case "password":
        if (!value) return "Password is required";
        if (value.length < 8) return "Password must be at least 8 characters";
        return undefined;
        
      case "confirmPassword":
        if (!value) return "Please confirm password";
        if (value !== formData.password) return "Passwords do not match";
        return undefined;
        
      default:
        return undefined;
    }
  };
  
  const handleChange = (field: FormField) => (
    e: React.ChangeEvent<HTMLInputElement>
  ) => {
    const value = e.target.value;
    
    setFormData(prev => ({
      ...prev,
      [field]: value
    }));
    
    // Validate on change if field was touched
    if (touched[field]) {
      const error = validateField(field, value);
      setErrors(prev => ({
        ...prev,
        [field]: error
      }));
    }
  };
  
  const handleBlur = (field: FormField) => () => {
    setTouched(prev => ({
      ...prev,
      [field]: true
    }));
    
    const error = validateField(field, formData[field]);
    setErrors(prev => ({
      ...prev,
      [field]: error
    }));
  };
  
  const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
    e.preventDefault();
    
    // Validate all fields
    const newErrors: FormErrors = {};
    let hasErrors = false;
    
    (Object.keys(formData) as FormField[]).forEach(field => {
      const error = validateField(field, formData[field]);
      if (error) {
        newErrors[field] = error;
        hasErrors = true;
      }
    });
    
    setErrors(newErrors);
    
    if (!hasErrors) {
      console.log("Form submitted:", formData);
      // Submit to API
    }
  };
  
  return (
    <form onSubmit={handleSubmit} className="registration-form">
      <h2>Register</h2>
      
      <div className="form-group">
        <input
          type="text"
          value={formData.username}
          onChange={handleChange("username")}
          onBlur={handleBlur("username")}
          placeholder="Username"
        />
        {touched.username && errors.username && (
          <span className="error">{errors.username}</span>
        )}
      </div>
      
      <div className="form-group">
        <input
          type="email"
          value={formData.email}
          onChange={handleChange("email")}
          onBlur={handleBlur("email")}
          placeholder="Email"
        />
        {touched.email && errors.email && (
          <span className="error">{errors.email}</span>
        )}
      </div>
      
      <div className="form-group">
        <input
          type="password"
          value={formData.password}
          onChange={handleChange("password")}
          onBlur={handleBlur("password")}
          placeholder="Password"
        />
        {touched.password && errors.password && (
          <span className="error">{errors.password}</span>
        )}
      </div>
      
      <div className="form-group">
        <input
          type="password"
          value={formData.confirmPassword}
          onChange={handleChange("confirmPassword")}
          onBlur={handleBlur("confirmPassword")}
          placeholder="Confirm Password"
        />
        {touched.confirmPassword && errors.confirmPassword && (
          <span className="error">{errors.confirmPassword}</span>
        )}
      </div>
      
      <button type="submit">Register</button>
    </form>
  );
}

export default RegistrationForm;

Test Your Knowledge

Type Safety Check

Which React TypeScript component is correctly typed?

// Option A
interface UserProps {
  name: string;
  age: number;
}

function User({ name, age }: UserProps) {
  return <div>{name} is {age} years old</div>;
}

// Option B
function Greeting(props) {
  return <h1>Hello, {props.name}</h1>;
}

// Option C
interface ButtonProps {
  onClick: () => void;
  children: React.ReactNode;
}

const Button: React.FC<ButtonProps> = ({ onClick, children }) => {
  return <button onClick={onClick}>{children}</button>;
};

// Option D
function Counter() {
  const [count, setCount] = useState(0);
  
  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={() => setCount(count + 1)}>Increment</button>
    </div>
  );
}

Common TypeScript Error

Code with Error
interface UserCardProps {
  name: string;
  age: number;
  email: string;
}

function UserCard({ name, age }: UserCardProps) {
  return (
    <div>
      <h2>{name}</h2>
      <p>Age: {age}</p>
      <p>Email: {email}</p>  {/* ❌ Error: email not destructured */}
    </div>
  );
}

// Using the component with wrong prop types
function App() {
  return (
    <UserCard 
      name="Yusuf" 
      age="25"  {/* ❌ Error: age should be number, not string */}
      // email prop is missing! ❌ Error
    />
  );
}

❌ Property 'email' does not exist. Type 'string' is not assignable to type 'number'. Property 'email' is missing in type.

What's Wrong?

Props must match the interface exactly. All required props must be provided, types must match, and destructured props must include all used properties.

Corrected Code
interface UserCardProps {
  name: string;
  age: number;
  email: string;
}

// ✅ Destructure all props
function UserCard({ name, age, email }: UserCardProps) {
  return (
    <div>
      <h2>{name}</h2>
      <p>Age: {age}</p>
      <p>Email: {email}</p>  {/* ✅ Now accessible */}
    </div>
  );
}

// ✅ Pass all required props with correct types
function App() {
  return (
    <UserCard 
      name="Yusuf" 
      age={25}  {/* ✅ Number, not string */}
      email="yusuf@example.com"  {/* ✅ All props provided */}
    />
  );
}

// Optional props
interface ButtonProps {
  text: string;
  variant?: "primary" | "secondary";  // Optional
  onClick?: () => void;  // Optional
}

function Button({ text, variant = "primary", onClick }: ButtonProps) {
  return (
    <button 
      className={`btn btn-${variant}`}
      onClick={onClick}
    >
      {text}
    </button>
  );
}

// ✅ Only required props needed
<Button text="Click me" />

// ✅ Can provide optional props
<Button 
  text="Submit" 
  variant="primary" 
  onClick={() => console.log("Clicked")} 
/>

Solution: Destructure all props and pass correct types

Best Practices

  1. Always type component props - use interfaces for clarity
  2. Use type inference when possible - let TypeScript infer from initial values
  3. Type event handlers explicitly - prevents runtime errors
  4. Make optional props explicit with ? operator
  5. Use React.ReactNode for children - most flexible type
  6. Type custom hooks with generics - reusable and type-safe
  7. Use proper ref types - HTMLElement types for DOM refs
  8. Clean up effects - return cleanup functions
best-practices-example.tsx
import React, { useState, useEffect } from 'react';

// ✅ Good: Clear props interface
interface UserCardProps {
  user: {
    id: number;
    name: string;
  };
  onEdit?: () => void;  // Optional
}

function UserCard({ user, onEdit }: UserCardProps) {
  return (
    <div>
      <h2>{user.name}</h2>
      {onEdit && <button onClick={onEdit}>Edit</button>}
    </div>
  );
}

// ✅ Good: Type inference for simple state
function Counter() {
  const [count, setCount] = useState(0);  // Inferred as number
  return <button onClick={() => setCount(count + 1)}>{count}</button>;
}

// ✅ Good: Explicit types for complex state
interface Todo {
  id: number;
  text: string;
}

function TodoApp() {
  const [todos, setTodos] = useState<Todo[]>([]);
  // ...
}

// ✅ Good: Typed event handlers
function Form() {
  const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
    e.preventDefault();
  };
  
  const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
    console.log(e.target.value);
  };
  
  return <form onSubmit={handleSubmit}>{/* ... */}</form>;
}

// ❌ Avoid: Untyped props
function Bad(props: any) {  // Don't use 'any'!
  return <div>{props.name}</div>;
}

// ❌ Avoid: Missing types for complex state
function AlsoBad() {
  const [data, setData] = useState();  // Type is 'undefined'!
  // Should be: useState<DataType | null>(null)
}

Key Takeaways

  • Type component props with interfaces for clarity
  • Use React.FC<Props> or inline prop typing
  • React.ReactNode is the most flexible children type
  • Event handlers need explicit types like React.ChangeEvent
  • useState infers types from initial values
  • Use generic type parameters for complex state
  • useRef needs proper HTML element types
  • Custom hooks can use generics for reusability
  • Always return cleanup functions in useEffect
  • TypeScript + React = excellent developer experience

What's Next?

Excellent work! You've mastered TypeScript with React and can now build type-safe React applications. Next, we'll explore Async/Await and Promises—how to type asynchronous code, handle Promises, and work with async/await in TypeScript. This is essential for modern JavaScript development!

Get ready to master asynchronous TypeScript!

Mastering TypeScript with React! Check this out!

Previous
Working with Modules
Next
Async/Await and Promises

Never Miss a New TypeScript Tutorial

Join 1,000+ developers learning TypeScript step-by-step. Get new tutorials, type safety tips, and exclusive resources delivered to your inbox - completely FREE.

You might also like

UI Components

Ready-made components for your projects

Developer Tools

Boost productivity with handy generators

HTML Game

Learn HTML concepts through interactive gameplay

Join Our Web Development Training Program

Master frontend & backend development with hands-on projects.

TypeScript Tutorials

0 of 30 completed

Your Progress0%

Topics

Getting Started

  • What is TypeScript?
  • TypeScript vs JavaScript
  • Setting Up TypeScript
  • Your First TypeScript Program

Basic Types

  • Primitive Types
  • Arrays and Tuples
  • Objects and Type Aliases
  • Enums
  • Any, Unknown, and Never

Functions & Interfaces

  • Function Types and Signatures
  • Interfaces
  • Optional and Default Parameters
  • Function Overloading

Advanced Types

  • Union and Intersection Types
  • Type Guards and Narrowing
  • Literal Types
  • Type Assertions and Casting

Generics

  • Introduction to Generics
  • Generic Constraints
  • Utility Types

Classes & OOP

  • Classes and Constructors
  • Access Modifiers
  • Inheritance and Polymorphism
  • Abstract Classes and Interfaces

Practical TypeScript

  • Working with Modules
  • TypeScript with React
  • Async/Await and Promises
  • Working with APIs and JSON
  • Type Declarations and DefinitelyTyped
  • TypeScript Best Practices
Falytom logoFALYTOM

Learn technologies like HTML, CSS, JavaScript, React, NodeJS and more through interactive games, tutorials, components and free tools. Transform your businesses with AI chatbots, SEO-optimized websites, and professional development services.

© 2025 Tomilola Group
  • Facebook logo
  • Twitter logo
  • Instagram logo
  • LinkedIn logo
  • GitHub logo
  • YouTube logo