React  

Using React with TypeScript – Common Patterns Every Developer Should Know

JavaScript powers the web, but it comes with a big tradeoff: it’s dynamically typed. That means type errors like passing a string where a number was expected only show up at runtime. For small scripts, this isn’t a big deal. But in large React applications, these issues can cause frustrating bugs that are hard to trace.

That’s where TypeScript comes in. TypeScript adds static typing to JavaScript, which means you catch errors earlier, improve editor autocomplete, and make your codebase easier to maintain. When combined with React, TypeScript can significantly improve how you work with components, state, and props.

In this article, we’ll explore common patterns for using React with TypeScript, including props, state, events, children, and custom hooks. Along the way, we’ll look at best practices and mistakes to avoid.

Why TypeScript with React?

Before diving into patterns, let’s understand the main benefits of using TypeScript in React projects:

  • Type safety: Catch errors at compile time rather than runtime.

  • Better autocomplete: Your editor knows exactly what props or state fields exist.

  • Self-documenting code: Types act as living documentation for your components.

  • Refactor with confidence: Changing a type forces updates in all dependent code.

Pattern 1. Typing Props

Props are the foundation of React components, and TypeScript makes them safer.

Example

type ButtonProps = {
  label: string;
  onClick: () => void;
  disabled?: boolean;
};

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

Here

  • label It is required and must be a string.

  • onClick It is required and must be a function.

  • disabled is optional, with a default of false.

If you forget label or onClick. TypeScript raises an error.

Pattern 2. Typing State with useState

React’s useState hook is strongly typed, but TypeScript sometimes needs a hint.

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

Here, the state must always be a number.

For a more complex state:

type User = { name: string; age: number };

const [user, setUser] = React.useState<User | null>(null);

This state can either be an User object or null. This prevents you from accidentally calling user.name when no user exists.

Pattern 3. Typing Events

Event handlers are a common source of confusion in React + TypeScript. Luckily, React provides built-in types.

Example: typing an input change event:

function TextInput() {
  const [value, setValue] = React.useState("");

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

  return <input type="text" value={value} onChange={handleChange} />;
}

Other common event types:

  • React.MouseEvent<HTMLButtonElement> For button clicks.

  • React.FormEvent<HTMLFormElement> For form submissions.

Pattern 4. Typing Components with Children

Many components wrap others using children. In TypeScript, you can type this with React.ReactNode.

type CardProps = {
  title: string;
  children: React.ReactNode;
};

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

This ensures Card can accept text, JSX, or other components.

Pattern 5. Typing Custom Hooks

Custom hooks are where TypeScript shines because you can define both input and output types.

Example: localStorage hook:

function useLocalStorage<T>(key: string, initialValue: T) {
  const [value, setValue] = React.useState<T>(() => {
    const saved = localStorage.getItem(key);
    return saved ? JSON.parse(saved) : initialValue;
  });

  React.useEffect(() => {
    localStorage.setItem(key, JSON.stringify(value));
  }, [key, value]);

  return [value, setValue] as const;
}

// usage
const [theme, setTheme] = useLocalStorage<"light" | "dark">("theme", "light");

Notice the <T>, This makes the hook generic so it can work with any type.

Pattern 6. Typing Context

React Context works seamlessly with TypeScript. Example:

type AuthContextType = {
  user: { name: string } | null;
  login: (name: string) => void;
  logout: () => void;
};

const AuthContext = React.createContext<AuthContextType | undefined>(undefined);

function AuthProvider({ children }: { children: React.ReactNode }) {
  const [user, setUser] = React.useState<{ name: string } | null>(null);

  const login = (name: string) => setUser({ name });
  const logout = () => setUser(null);

  return (
    <AuthContext.Provider value={{ user, login, logout }}>
      {children}
    </AuthContext.Provider>
  );
}

function useAuth() {
  const context = React.useContext(AuthContext);
  if (!context) throw new Error("useAuth must be used within AuthProvider");
  return context;
}

This ensures any component using it useAuth knows exactly what methods and data are available.

Pattern 7. Typing Refs

Refs can be typed too. Example:

function InputFocus() {
  const inputRef = React.useRef<HTMLInputElement>(null);

  const focusInput = () => {
    inputRef.current?.focus();
  };

  return (
    <>
      <input ref={inputRef} />
      <button onClick={focusInput}>Focus</button>
    </>
  );
}

Here, inputRef is explicitly typed as an HTMLInputElement.

Best Practices

  • Use type aliases (type) for props and state. Use interfaces when extending.

  • Use union types for limited sets of values.

  • Write generic hooks for reusable typed logic.

  • Keep context types small to avoid re-renders.

  • Let TypeScript infer when it can.

Conclusion

TypeScript transforms the way you write React applications. By typing props, state, events, children, context, and hooks, you make your components safer and more predictable. The key is to use typing wisely, avoid any, don’t overcomplicate types, and let TypeScript infer where possible.

The result is code that’s not only less error-prone but also easier to read and maintain. As your React projects scale, TypeScript becomes less of an optional extra and more of an essential tool.

If you’re building production-grade React apps, embracing TypeScript is one of the smartest moves you can make.