Typing custom hooks in React can be a bit tricky at first, especially if you want to maintain type safety and clarity across your codebase. Over the years, I've seen developers either avoid typing hooks altogether or overcomplicate their types, which leads to brittle or unreadable code. Getting this right is crucial not just for catching bugs early but also for making your hooks reusable and maintainable in larger projects.
Let's break down how to type custom hooks effectively, why it matters, and some practical tips and pitfalls I've encountered in real-world projects.
Custom hooks are essentially JavaScript functions that use React hooks internally and return some state, functions, or values. Typing them properly means:
Without proper typing, you risk passing around ambiguous data, which leads to bugs that are hard to track down.
At its core, a custom hook is a function, so typing it is similar to typing any other function in TypeScript. The main difference is the return type, which often includes state values, setters, or callback functions.
Here’s a simple example of a typed custom hook:
import { useState } from 'react';
function useCounter(initialValue: number = 0): [number, () => void, () => void] {
const [count, setCount] = useState(initialValue);
const increment = () => setCount(prev => prev + 1);
const decrement = () => setCount(prev => prev - 1);
return [count, increment, decrement];
}
In this example:
initialValue is typed as number with a default.[number, () => void, () => void].This pattern works well for simple hooks, but for more complex hooks, you might want to use interfaces or type aliases for clarity.
When your hook returns an object (which is common), defining a type or interface helps readability:
interface UseFormReturn {
values: Record<string, any>;
handleChange: (e: React.ChangeEvent<HTMLInputElement>) => void;
resetForm: () => void;
}
function useForm(initialValues: Record<string, any>): UseFormReturn {
const [values, setValues] = useState(initialValues);
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const { name, value } = e.target;
setValues(prev => ({ ...prev, [name]: value }));
};
const resetForm = () => setValues(initialValues);
return { values, handleChange, resetForm };
}
Here, the interface UseFormReturn clearly defines what the hook returns, making it easier to maintain and understand.
One of the most powerful features when typing custom hooks is using generics. This is especially useful when your hook needs to work with various data shapes.
For example, a form hook that should accept any shape of form data:
function useForm<T extends Record<string, any>>(initialValues: T) {
const [values, setValues] = useState<T>(initialValues);
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const { name, value } = e.target;
setValues(prev => ({ ...prev, [name]: value }));
};
const resetForm = () => setValues(initialValues);
return { values, handleChange, resetForm };
}
Now, when you use useForm, TypeScript infers the shape of your form data, giving you full type safety:
const { values, handleChange } = useForm({ username: '', age: 0 });
// values.username is string, values.age is number
This pattern is very common in production apps where forms or data shapes vary a lot.
any types, which defeats the purpose of using TypeScript.any: While tempting, using any too liberally reduces type safety and can cause bugs later.React.ChangeEvent vs React.MouseEvent) can cause subtle bugs.Typing itself doesn’t impact runtime performance, but well-typed hooks can help you write more efficient code. For example:
useCallback to avoid unnecessary re-renders.Here’s a quick example:
import { useState, useCallback } from 'react';
function useToggle(initialValue: boolean = false): [boolean, () => void] {
const [value, setValue] = useState(initialValue);
const toggle = useCallback(() => setValue(v => !v), []);
return [value, toggle];
}
Using useCallback ensures the toggle function identity is stable, which is helpful if you pass it down to memoized components.
Typing custom hooks doesn’t directly affect security, but it can help prevent bugs that might lead to security issues, such as:
Always be mindful of what your hooks expose and avoid leaking sensitive information unintentionally.
useForm, useToggle, or useFetch to illustrate your points.In production, I often write hooks that:
Here’s a more advanced example of a typed data fetching hook:
import { useState, useEffect } from 'react';
interface FetchState<T> {
data: T | null;
loading: boolean;
error: Error | null;
}
function useFetch<T = unknown>(url: string): FetchState<T> {
const [data, setData] = useState<T | null>(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<Error | null>(null);
useEffect(() => {
setLoading(true);
fetch(url)
.then(res => {
if (!res.ok) {
throw new Error('Network response was not ok');
}
return res.json() as Promise<T>;
})
.then(setData)
.catch(setError)
.finally(() => setLoading(false));
}, [url]);
return { data, loading, error };
}
This hook uses generics to type the data it fetches, which is very common in real apps. It also handles loading and error states, making it a reusable building block.
| Aspect | Typed Hooks | Untyped Hooks |
|---|---|---|
| Developer Experience | Autocomplete, type checking, fewer runtime bugs | No autocomplete, more prone to errors |
| Maintainability | Easier to refactor and understand | Harder to maintain, especially in large codebases |
| Reusability | Generics enable flexible, reusable hooks | Limited reusability, often duplicated code |
| Complexity | Requires upfront effort to define types | Faster to write initially but risky long-term |
Typing custom hooks is about balancing safety, clarity, and flexibility. Using TypeScript generics, interfaces, and proper event types helps you build hooks that are easy to use and maintain. Avoid common pitfalls like overusing any or skipping return types. In interviews, focus on explaining your choices and demonstrating practical examples. In production, well-typed hooks save time by catching bugs early and improving collaboration.