Skip to content

Instantly share code, notes, and snippets.

@29Kumait
Last active April 26, 2024 21:58
Show Gist options
  • Save 29Kumait/81bf67f48ddcf7c81a9e2170e4963153 to your computer and use it in GitHub Desktop.
Save 29Kumait/81bf67f48ddcf7c81a9e2170e4963153 to your computer and use it in GitHub Desktop.
TypeScript & React concepts with practical example.

1. Interfaces for Component Props

interface ProductProps {
  name: string;
  price: number;
  imageUrl: string;
  inStock: boolean;
}

const ProductCard: React.FC<ProductProps> = ({ name, price, imageUrl, inStock }) => {
  return (
    <div>
      <h2>{name}</h2>
      <img src={imageUrl} alt={name} />
      <p>Price: ${price}</p>
      {inStock ? <p>In Stock</p> : <p>Out of Stock</p>}
    </div>
  );
};
  • The ProductProps interface enforces structure and type safety for props passed to the ProductCard component.
  • This ensures consistency, prevents runtime type errors, and enhances code readability.

2. Generics for Reusable Components

interface ItemData<T> {
  id: number;
  value: T;
}

const ListComponent: React.FC<{ data: ItemData<any>[] }> = ({ data }) => {
  return (
    <ul>
      {data.map((item) => (
        <li key={item.id}>{item.value}</li>
      ))}
    </ul>
  );
};
  • The ListComponent leverages generics () to work with various data types (numbers, strings, custom objects, etc.)
  • Example usage: <ListComponent data={[{id: 1, value: 'Product A'}]} />

3. Typing React Hooks

const [count, setCount] = useState<number>(0); 

const handleIncrement = () => {
  setCount(count + 1); 
};
  • useState specifies that the count state variable holds a number.
  • TypeScript ensures type-correct updates and usage throughout the component.

4. Typing Events

const handleInputChange = (event: React.ChangeEvent<HTMLInputElement>) => {
    setNewValue(event.target.value);
};

return (
  <input type="text" value={newValue} onChange={handleInputChange} />   
);
  • The event object is accurately typed for type-safe access to input values.

1. Union Types for Flexible Props

interface ButtonProps {
  label: string;
  onClick: () => void;
  size?: 'small' | 'medium' | 'large';  // Optional size property
  variant?: 'primary' | 'secondary';    // Optional variant
}

const Button: React.FC<ButtonProps> = ({ label, onClick, size, variant }) => {
   // ... button rendering with size and variant styles
};
  • Union types (|) allow multiple possible values for props, boosting component adaptability.

2. Conditional Types for Dynamic Typing

type FetchStatus<T> = 
    | { status: 'idle' }
    | { status: 'loading' }
    | { status: 'success'; data: T }
    | { status: 'error'; error: Error }; 

const useFetchData = <T>(): FetchStatus<T> => {
    // ... data fetching logic
};  
  • Conditional types change the resulting type based on logic within the type definition, perfect for modeling states like fetch results.

3. Utility Types for Type Manipulation

type FormValues = {
  name: string;
  email: string;
}

const initialValues: Partial<FormValues> = {}; // all properties become optional

const readonlyProps: Readonly<FormValues> = { 
   name: 'Alice', 
   email: 'alice@example.com' 
}; // all properties are read-only
  • Partial makes object properties optional.
  • Readonly prevents property modification.

1. Union Types for Flexible Props

Key Idea: Sometimes a component prop might accept several discrete values. Union types let us accurately model and handle these scenarios.

Example Breakdown:

  • The size prop can be 'small', 'medium', 'large', or even undefined when not explicitly set. Union types capture this flexibility.
  • The variant prop has similar behavior.

Benefits:

  • Developers using the Button component get clear type hints about valid prop values.
  • TypeScript will warn if you try to pass an invalid value (e.g., size='extra-large').
  • Your component's logic can handle these variations in a type-safe manner using conditional checks (e.g., if (size == 'large') { ... }).

2. Conditional Types for Dynamic Typing

Key Idea: Conditional types are like "if statements" within TypeScript's type system, allowing you to express logic when defining types.

Example Breakdown:

  • FetchStatus<T> represents the different states a data-fetching operation can be in:
    • 'idle': No fetch in progress.
    • 'loading': Fetch is ongoing.
    • 'success': Fetch succeeded, and data holds the result of type T.
    • 'error': The fetch failed, and error holds the Error object.

Benefits:

  • The return type of useFetchData accurately reflects the component's state at any given time.
  • TypeScript will prevent you from trying to access data when the status is not 'success'.

3. Utility Types for Type Manipulation

Key Idea: TypeScript offers built-in utility types that streamline common type transformations.

Example Breakdown:

  • Partial<FormValues> creates a type where every property from FormValues becomes optional. This is great for default values or incomplete form submission scenarios.
  • Readonly<FormValues> makes every property in FormValues read-only. This is useful when you need to pass form data around but prevent accidental modifications.

Benefits:

  • Avoids tedious manual type definitions.
  • Promotes type safety and maintainability by signaling your intent (e.g., indicating immutable data).

Advanced TypeScript concepts translate into more realistic React application scenarios:

1. Union Types for Flexible Form Controls TypeScript

interface FormFieldProps {
  label: string;
  name: string;
  type?: 'text' | 'email' | 'password' | 'textarea' | 'select'; 
}

const FormField: React.FC<FormFieldProps> = ({ label, name, type = 'text', ...otherProps }) => {
  // ... Logic to render the appropriate input/select/textarea based on 'type' prop
};
  • This makes a single FormField component handle various input types.
  • Your form code becomes more maintainable, avoiding redundant, type-specific components.

2. Conditional Types for API Responses

// Example API response
interface Product {
  id: number;
  name: string;
  // ... other product details
}

type ProductFetchStatus = 
    | { status: 'idle' }
    | { status: 'loading' }
    | { status: 'success'; data: Product[] } // Success with array of products
    | { status: 'error'; error: Error }; 

const useFetchProducts = (): ProductFetchStatus => {
    // ... logic to fetch products from an API
};   
  • The ProductFetchStatus accurately models the state of fetching products from an API.
  • Your component can gracefully handle loading, error, and success cases based on the status, providing a type-safe and user-friendly experience.

3. Utility Types for Complex State Management

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

const initialTodos: Readonly<Todo[]> = [/* ... */];  

// Reducer actions with payloads using utility types
type AddTodoAction = { type: 'ADD_TODO'; payload: Pick<Todo, 'text'> };
type ToggleTodoAction = { type: 'TOGGLE_TODO'; payload: Pick<Todo, 'id'> };
  • Readonly<Todo[]> ensures your initial state cannot be accidentally modified.
  • Pick assists in constructing action payloads by selecting specific properties from existing types, improving reducer code clarity and type safety.

Conditional Types for API Responses

Example: a component that fetches a list of products from an API and displays them, handling loading and error states

import React, { useState, useEffect } from 'react';

// Simplified API interaction (you'd likely use axios or fetch)
const fetchProducts = async (): Promise<Product[]> => {
  // Simulate delayed fetch
  await new Promise((resolve) => setTimeout(resolve, 1000)); 

  // Replace with your actual API call
  return [ 
    { id: 1, name: 'Product A', price: 9.99 },
    { id: 2, name: 'Product B', price: 14.99 },
  ];  
};

// ... Product type definition from earlier  

type ProductFetchStatus = 
    | { status: 'idle' }
    | { status: 'loading' }
    | { status: 'success'; data: Product[] }
    | { status: 'error'; error: Error }; 

const ProductList: React.FC = () => {
  const [status, setStatus] = useState<ProductFetchStatus>({ status: 'idle' });

  useEffect(() => {
    const fetchData = async () => {
      setStatus({ status: 'loading' });
      try {
        const data = await fetchProducts();
        setStatus({ status: 'success', data });
      } catch (error) {
        setStatus({ status: 'error', error }); 
      }
    };

    fetchData();
  }, []); 

  return (
    <div>
      {status.status === 'loading' && <p>Loading...</p>}
      {status.status === 'success' && (
        <ul>
          {status.data.map((product) => (
            <li key={product.id}>
              {product.name} - ${product.price}
            </li>
          ))}
        </ul>
      )}
      {status.status === 'error' && <p>Error: {status.error.message}</p>} 
    </div>
  );
};

export default ProductList;

Explanation:

  • Conditional Type: ProductFetchStatus models the possible states of the fetch operation.
  • State Management: useState tracks the fetch status.
  • Fetching Logic: useEffect handles fetching data on component mount, updating state through the various stages.
  • Conditional Rendering: The JSX conditionally renders elements based on the status, providing loading feedback, an error message, or the fetched product list.

Key Benefit: The combination of TypeScript and React's state management allows your component to clearly represent the flow of data fetching, enhancing type safety and user experience.


Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment