Intermediate90 min read

Advanced TypeScript Patterns in React

Learn advanced TypeScript patterns for React: generics, utility types, type guards, and complex component typing.

Topics Covered:

GenericsUtility TypesType GuardsAdvanced PatternsType Narrowing

Prerequisites:

  • TypeScript Basics for React Developers
  • Component Composition and Context

Video Tutorial

Overview

As you build more complex React applications, you'll encounter situations that require advanced TypeScript patterns. This tutorial covers generics for reusable components, utility types for transforming types, type guards for runtime type checking, and advanced patterns for complex scenarios. You'll learn how to create flexible, type-safe components that work with different data types while maintaining full type safety.

Lesson 1: Generics in React Components

Generics allow you to create reusable components that work with different types while maintaining type safety. What are Generics? • Type parameters that make components flexible • Maintain type safety while working with different types • Written with angle brackets: <T> Use Cases: • Lists with different item types • Data fetchers for different API responses • Form components with different value types • Utility hooks that work with any type Benefits: • Type safety preserved • Code reuse • Better autocomplete • Catch errors at compile time

Code Example:
// Generic List Component
interface ListProps<T> {
  items: T[];
  renderItem: (item: T) => React.ReactNode;
  keyExtractor: (item: T) => string | number;
}

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

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

interface Product {
  sku: string;
  title: string;
  price: number;
}

function App() {
  const users: User[] = [
    { id: 1, name: "Alice" },
    { id: 2, name: "Bob" },
  ];
  
  const products: Product[] = [
    { sku: "PROD-1", title: "Widget", price: 29.99 },
    { sku: "PROD-2", title: "Gadget", price: 49.99 },
  ];
  
  return (
    <div>
      <List
        items={users}
        renderItem={(user) => <span>{user.name}</span>}
        keyExtractor={(user) => user.id}
      />
      
      <List
        items={products}
        renderItem={(product) => (
          <span>
            {product.title} - {'$'}{product.price}
          </span>
        )}
        keyExtractor={(product) => product.sku}
      />
    </div>
  );
}

// Generic Hook
function useFetch<T>(url: string) {
  const [data, setData] = useState<T | null>(null);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState<Error | null>(null);
  
  useEffect(() => {
    fetch(url)
      .then((res) => res.json())
      .then((data: T) => {
        setData(data);
        setLoading(false);
      })
      .catch((err: Error) => {
        setError(err);
        setLoading(false);
      });
  }, [url]);
  
  return { data, loading, error };
}

// Usage
function UserProfile({ userId }: { userId: number }) {
  const { data: user, loading, error } = useFetch<User>(
    `/api/users/${userId}`
  );
  
  if (loading) return <div>Loading...</div>;
  if (error) return <div>Error: {error.message}</div>;
  if (!user) return <div>Not found</div>;
  
  return <div>{user.name}</div>; // TypeScript knows user is User type
}

Generics make components flexible and reusable while maintaining type safety. Use angle brackets <T> to define type parameters. TypeScript infers the generic type from usage, preserving full type safety.

Lesson 2: Utility Types

TypeScript provides utility types that transform existing types. These are incredibly useful for React development. Common Utility Types: • Partial<T>: Makes all properties optional • Required<T>: Makes all properties required • Pick<T, K>: Select specific properties • Omit<T, K>: Remove specific properties • Readonly<T>: Make all properties readonly • Record<K, V>: Create object type with specific keys Use Cases: • Form state (Partial of main type) • Update functions (Partial of entity) • Component variants (Pick specific props) • Configuration objects (Record types) • Immutable data (Readonly)

Code Example:
// Base interface
interface User {
  id: number;
  name: string;
  email: string;
  age: number;
  role: "admin" | "user";
}

// Partial: All properties optional (useful for forms)
type PartialUser = Partial<User>;
// Equivalent to:
// {
//   id?: number;
//   name?: string;
//   email?: string;
//   age?: number;
//   role?: "admin" | "user";
// }

function EditUserForm({ user }: { user: User }) {
  const [formData, setFormData] = useState<Partial<User>>({
    name: user.name,
    email: user.email,
    // Other fields optional
  });
  
  return (
    <form>
      <input
        value={formData.name || ""}
        onChange={(e) =>
          setFormData({ ...formData, name: e.target.value })
        }
      />
    </form>
  );
}

// Pick: Select specific properties
type UserPreview = Pick<User, "id" | "name" | "email">;
// Only has: id, name, email

function UserCard({ user }: { user: UserPreview }) {
  return (
    <div>
      <h3>{user.name}</h3>
      <p>{user.email}</p>
    </div>
  );
}

// Omit: Remove specific properties
type UserWithoutId = Omit<User, "id">;
// Has everything except id

function CreateUserForm() {
  const [formData, setFormData] = useState<UserWithoutId>({
    name: "",
    email: "",
    age: 0,
    role: "user",
  });
  
  // id will be generated on server
}

// Required: Make all properties required
interface Config {
  apiUrl?: string;
  timeout?: number;
}

type RequiredConfig = Required<Config>;
// apiUrl and timeout are now required

// Record: Create object type with specific keys
type StatusColors = Record<"success" | "error" | "warning", string>;
// Equivalent to:
// {
//   success: string;
//   error: string;
//   warning: string;
// }

const colors: StatusColors = {
  success: "green",
  error: "red",
  warning: "yellow",
};

// Readonly: Make all properties readonly
type ImmutableUser = Readonly<User>;
// Cannot modify properties after creation

Utility types transform existing types. Partial is great for forms, Pick/Omit for selective properties, Record for key-value objects, and Readonly for immutable data. These utilities help create precise types without duplication.

Lesson 3: Type Guards and Type Narrowing

Type guards are functions that check types at runtime and narrow TypeScript's type checking accordingly. What are Type Guards? • Functions that return type predicates • Narrow types based on runtime checks • Make code type-safe with runtime validation Common Patterns: • typeof checks • instanceof checks • Property checks • Custom type predicates • Discriminated unions Use Cases: • API response validation • Form validation • Error handling • Conditional rendering

Code Example:
// Type guards
function isString(value: unknown): value is string {
  return typeof value === "string";
}

function isUser(value: unknown): value is User {
  return (
    typeof value === "object" &&
    value !== null &&
    "id" in value &&
    "name" in value &&
    "email" in value
  );
}

// Using type guards
function processData(data: unknown) {
  if (isString(data)) {
    // TypeScript knows data is string here
    console.log(data.toUpperCase());
  }
  
  if (isUser(data)) {
    // TypeScript knows data is User here
    console.log(data.name, data.email);
  }
}

// Discriminated unions
type ApiResponse<T> =
  | { status: "success"; data: T }
  | { status: "error"; error: string }
  | { status: "loading" };

function handleResponse<T>(response: ApiResponse<T>) {
  if (response.status === "success") {
    // TypeScript knows data exists
    return response.data;
  } else if (response.status === "error") {
    // TypeScript knows error exists
    throw new Error(response.error);
  } else {
    // TypeScript knows this is loading state
    return null;
  }
}

// React component with type guards
function DataDisplay({ data }: { data: unknown }) {
  if (isUser(data)) {
    return (
      <div>
        <h2>{data.name}</h2>
        <p>{data.email}</p>
      </div>
    );
  }
  
  if (isString(data)) {
    return <p>{data}</p>;
  }
  
  return <div>Unknown data type</div>;
}

// Form validation with type guards
function validateEmail(email: unknown): email is string {
  return (
    isString(email) &&
    email.includes("@") &&
    email.length > 5
  );
}

function EmailInput() {
  const [email, setEmail] = useState<string>("");
  const [isValid, setIsValid] = useState(false);
  
  const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
    const value = e.target.value;
    setEmail(value);
    setIsValid(validateEmail(value));
  };
  
  return (
    <div>
      <input
        type="email"
        value={email}
        onChange={handleChange}
        aria-invalid={!isValid}
      />
      {isValid && <span>✓ Valid email</span>}
    </div>
  );
}

Type guards narrow types based on runtime checks. Use type predicates (value is Type) to tell TypeScript what type something is after checking. Discriminated unions work great with type narrowing for handling different states or response types.

Lesson 4: Advanced Component Patterns

Advanced TypeScript patterns for complex React components. Patterns Covered: • Polymorphic components • Higher-order components with types • Render props pattern • Compound components • Forward refs with generics Best Practices: • Use generics for flexibility • Maintain type safety • Leverage utility types • Type event handlers precisely

Code Example:
// Polymorphic component
type PolymorphicProps<T extends React.ElementType> = {
  as?: T;
  children: React.ReactNode;
} & React.ComponentPropsWithoutRef<T>;

function Polymorphic<T extends React.ElementType = "div">({
  as,
  children,
  ...props
}: PolymorphicProps<T>) {
  const Component = as || "div";
  return <Component {...props}>{children}</Component>;
}

// Usage
<Polymorphic as="button" onClick={() => {}}>Click</Polymorphic>
<Polymorphic as="a" href="/link">Link</Polymorphic>
<Polymorphic>Default div</Polymorphic>

// Higher-order component with types
function withLoading<P extends object>(
  Component: React.ComponentType<P>
) {
  return function WithLoadingComponent(
    props: P & { loading?: boolean }
  ) {
    if (props.loading) {
      return <div>Loading...</div>;
    }
    
    const { loading, ...componentProps } = props;
    return <Component {...(componentProps as P)} />;
  };
}

// Render props pattern
interface DataProps<T> {
  url: string;
  children: (data: T | null, loading: boolean, error: Error | null) => React.ReactNode;
}

function DataFetcher<T>({ url, children }: DataProps<T>) {
  const [data, setData] = useState<T | null>(null);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState<Error | null>(null);
  
  useEffect(() => {
    fetch(url)
      .then((res) => res.json())
      .then((data: T) => setData(data))
      .catch((err) => setError(err))
      .finally(() => setLoading(false));
  }, [url]);
  
  return <>{children(data, loading, error)}</>;
}

// Usage
<DataFetcher<User> url="/api/user/1">
  {(user, loading, error) => {
    if (loading) return <div>Loading...</div>;
    if (error) return <div>Error: {error.message}</div>;
    if (!user) return <div>Not found</div>;
    return <div>{user.name}</div>;
  }}
</DataFetcher>

Advanced patterns like polymorphic components, HOCs, and render props can be fully type-safe with TypeScript. Use generics to maintain flexibility while preserving type safety. These patterns enable powerful, reusable components.

Conclusion

Advanced TypeScript patterns make your React code more flexible and type-safe. Generics enable reusable components, utility types transform types efficiently, type guards provide runtime safety, and advanced patterns unlock powerful component architectures. Remember: TypeScript's type system is powerful - leverage it to create robust, maintainable code. Start simple and add complexity only when needed.