TypeScript React: Patterns & Pitfalls


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 → staysnull
foreverForgetting event type → TS can’t help autocomplete
useRef(null)
without generic → stuck inany | null
land
Fix Tips:
Always set an explicit type when initial state is
null
Use
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 (
T
could 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 SomeType
to 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 if
s 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
| undefined
unions for control flow — use full shape insteadUse
switch
for 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
any
orstring
Using
React.FC
carelessly → implicit children, messy type inferenceForgetting to enforce structure in compound components
Fix Tips:
Prefer explicit
React.ReactNode
orReactElement
Use 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 Type
syntax to inform TS of guard outcomeLet TS infer return types when they’re obvious
Avoid using
as
unless 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
null
orundefined
statesForgetting to handle error/loading logic in JSX
Fix Tips:
Never type
data
as apromise<T>
inuseState
Use 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! 💥💥
Subscribe to my newsletter
Read articles from Faiaz Khan directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by
Faiaz Khan
Faiaz Khan
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.