TypeScript React: Patterns & Pitfalls

Hey! I'm Faiaz — a frontend developer who loves writing clean, efficient, and readable code (and sometimes slightly chaotic code, but only when debugging). This blog is my little corner of the internet where I share what I learn about React, JavaScript, modern web tools, and building better user experiences. When I'm not coding, I'm probably refactoring my to-do list or explaining closures to my cat. Thanks for stopping by — hope you find something useful or mildly entertaining here.
Your React code compiles. But now TypeScript is yelling at you like:
“You can’t assign a string to a number!”
“This might be null!”
“This property doesn’t exist on type ‘never’!”
You try to fight back… by slapping as any everywhere.
Let’s fix that. properly.
This post explores advanced TypeScript concepts in React, with a twist:
We’ll look at what each tool solves, the common mistakes devs make, and how to actually fix them.
Here’s what we’re covering:
Type safety — Props, state, refs, events
Generic in components
Discriminated unions
Component composition
Type inference + type guards
Typing async logic and useEffect
Type Safety in React — Props, State, Refs, Events
The Problem:
You want to pass props, use refs, and manage state — but your types are either too loose or too strict.
The Solution:
Strongly type everything React-related — with care.
Props:
type ButtonProps = {
label: string;
onClick: () => void;
};
const Button: React.FC<ButtonProps> = ({ label, onClick }) => (
<button onClick={onClick}>{label}</button>
);
useState:
const [count, setCount] = useState<number>(0); // ✅
Refs:
const inputRef = useRef<HTMLInputElement | null>(null);
Events:
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
console.log(e.target.value);
};
Common Mistakes:
Typing
useState(null)without providing a type → staysnullforeverForgetting event type → TS can’t help autocomplete
useRef(null)without generic → stuck inany | nullland
Fix Tips:
Always set an explicit type when initial state is
nullUse
React.ChangeEvent<T>orReact.MouseEvent<T>for handlesFor refs, use
useRef<Type | null>()with proper type
Generics in Components — Make ‘Em Flexible
The Problem:
You want a component (or hook) to accept any data type, but keep strong typing.
The Solution:
Use generics — like <T> — to create reusable, typed components.
Example:
type ListProps<T> = {
items: T[];
render: (item: T) => React.ReactNode;
};
function List<T>({ items, render }: ListProps<T>) {
return <ul>{items.map(render)}</ul>;
}
Usage:
<List items={['React', 'TypeScript']} render={(item) => <li>{item}</li>} />
Common Mistakes:
Using generics without constraints → unsafe access (
Tcould be anything)Overengineering simple types with too many generics
Forgetting to pass generic when calling the component/hook
Fix Tips:
Use constraints like
T extends SomeTypeto control shapeKeep generics as simple as possible
Let TS infer types when possible — but not at the cost of safety
Discriminated Unions — Power Your Conditional Logic
The Problem:
You have a component that takes multiple variants (e.g., type: “error” | “success“) — and you’re relying on optional props and ifs everywhere.
The Solution:
Use discriminated unions — TypeScript’s way of narrowing types automatically.
Example:
type Success = { status: 'success'; message: string };
type Error = { status: 'error'; code: number };
type ResultProps = Success | Error;
const Result = (props: ResultProps) => {
if (props.status === 'success') {
return <p>{props.message}</p>;
} else {
return <p>Error Code: {props.code}</p>;
}
};
Common Mistakes:
Forgetting to add the discriminated key (
status,type,kind)Using booleans or string literals inconsistently
Getting “properly X does not exist on type Y” errors
Fix Tips:
Always include a
”type”or”status”filed to distinguish typesAvoid
| undefinedunions for control flow — use full shape insteadUse
switchfor clear narrowing in complex scenarios
Component Composition + Types — Be Intentional
The Problem:
You want flexible, composable components — render props, compound components, or children-first APIs — but typing them is a mess.
The Solution:
Use ReactNode, ReactElement, and component-type inference properly.
Children:
type CardProps = {
children: React.ReactNode;
};
const Card = ({ children }: CardProps) => <div>{children}</div>;
Render props:
type RenderProp<T> = {
render: (item: T) => React.ReactNode;
};
Common Mistakes:
Typing children as
anyorstringUsing
React.FCcarelessly → implicit children, messy type inferenceForgetting to enforce structure in compound components
Fix Tips:
Prefer explicit
React.ReactNodeorReactElementUse utility types like
ComponentType<props>for render propsAvoid overusing
FC— be explicit when necessary
Type Inference + Type Guards — Let TS Work For You
The Problem:
You’re either typing everything manually or not at all — and wondering why undefined is not assignable to is ruining your day.
The Solution:
Let TS infer when it’s obvious — and use type guards to handle branching.
Inference:
const user = { name: 'Faiaz', age: 25 }; // inferred automatically
Custom Guard:
type Cat = { type: 'cat'; meow: () => void };
type Dog = { type: 'dog'; bark: () => void };
type Pet = Cat | Dog;
function isCat(pet: Pet): pet is Cat {
return pet.type === 'cat';
}
Common Mistakes:
Incomplete type guards that don’t narrow types properly
Overwriting good inference with bad manual annotations
Mixing inference and
any
Fix Tips:
Use
pet is Typesyntax to inform TS of guard outcomeLet TS infer return types when they’re obvious
Avoid using
asunless you’re absolutely sure
Handling Asnychronous Types — Don’t Race Yourself
The Problem:
You’re using fetch and axios, useEffect, async/await, but your types don’t reflect reality. Sometimes data is undefined, sometimes it’s a promise.
The Solution:
Use proper async typing — and separate data, loading, and error clearly.
Example:
type User = { name: string };
const [data, setData] = useState<User | null>(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
async function fetchUser() {
try {
const res = await fetch('/api/user');
const user: User = await res.json();
setData(user);
} finally {
setLoading(false);
}
}
fetchUser();
}, []);
Common Mistakes:
Typing
const data: Promise<User>→ wrong!Not accounting for
nullorundefinedstatesForgetting to handle error/loading logic in JSX
Fix Tips:
Never type
dataas apromise<T>inuseStateUse union types
(T | null)for pending statesBreak async logic into separate functions when possible
Final Words
TypeScript in React isn’t about perfection — it’s about building confidence in your code. The more you break things (and fix them the right way), the faster you grow.
Write types like your teammates are reading them.
Write bugs like nobody’s watching.
💬 Got a TypeScript horror story? Drop in the comments — let’s learn from it.
📚 More dev rants and React deep dives on usefaiaz.hashnode.dev
💻 Code examples & bonus snippets: github.com/Faiaz98
Happy typing! 💥💥



