Lesson 57 of 80
A custom hook that works for any data type needs a type parameter, not a wider type — the difference between useLocalStorage<T> knowing exactly what it stored and useLocalStorage(key, value: unknown) knowing nothing.
14 min read
Every option explains itself — why the right one is right, and why each wrong one is wrong. Miss one and it comes back later on a spaced-repetition schedule.
This topic is about judgment and practical tradeoffs more than a single right answer — a mock interview is the better way to test yourself on it.
Be interviewed on this →Hints without spoilers, and a plain explanation of why a wrong answer was wrong — answered only from this lesson. Free account, no card.
Sign up freeWhat interviewers actually ask about this, and what a strong answer has to cover.
A strong answer covers
Likely follow-ups
The TypeScript-with-React lesson covered typing components — props in, JSX out. A custom hook is a different shape of problem: it's a regular function that happens to call other hooks, and often the whole point of writing one is that it should work for any data type its caller hands it, while still giving that caller back something precisely typed. That's exactly what generics are for — a type parameter that isn't fixed by the hook's own definition, but supplied per call site.
useLocalStoragefunction useLocalStorage<T>(key: string, initialValue: T): [T, (value: T) => void] {
const [value, setValue] = useState<T>(() => {
const stored = localStorage.getItem(key);
return stored ? (JSON.parse(stored) as T) : initialValue;
});
const setStoredValue = (next: T) => {
setValue(next);
localStorage.setItem(key, JSON.stringify(next));
};
return [value, setStoredValue];
}
interface User {
id: string;
name: string;
}
const [user, setUser] = useLocalStorage<User>("user", { id: "1", name: "Ada" });
// user: User, setUser: (value: User) => void
const [count, setCount] = useLocalStorage("count", 0);
// T inferred as number from initialValue — no explicit <T> needed here
Two call sites, two completely different, fully-checked types — setUser only accepts a User, setCount only accepts a number — from one hook definition. T isn't declared anywhere inside useLocalStorage itself; it's a placeholder that gets filled in by whatever the caller passes as initialValue (or by an explicit <User> when there's no argument to infer it from, as with an initially-null value).
T is inferred from the argument, not declared. Most calls don't need useLocalStorage<User>(...) written out at all — TypeScript infers T from whatever initialValue is, the same generic-inference behavior from the generics lesson, just applied to a hook instead of a plain function.as T cast on JSON.parse is a trust cast, not a safety net. JSON.parse returns any; casting it to T tells TypeScript "believe me," but if localStorage actually holds mismatched or corrupted JSON (a previous version of the app stored a different shape), nothing here catches that at runtime — the type system's guarantee stops exactly where the cast starts.useLocalStorage still can't be called conditionally or inside a loop — that's a runtime/lint concern the type system has no opinion on, regardless of how generic the hook's signature is.This is the same generic-function mechanism from the core generics lesson — a type parameter filled in per call site — but where the TypeScript-with-React lesson applied that idea to typing components (a generic List<T> component), this lesson applies it to a hook function whose entire value is working identically for whatever type flows in from the caller. The explicit-tuple-return gotcha additionally reaches back to the tuples lesson: the exact same "positional types get lost without an annotation" trap, just showing up at a hook's return statement instead of a variable declaration.