This article is published in English.
Common TypeScript Errors in React and How to Resolve Them
A practical walkthrough of five recurring TypeScript error categories in React apps—props, events, state, async data, and children—with clear fixes for each.
If you're coming from JavaScript, TypeScript can feel intimidating at first. During the first few days, it can seem like the compiler is actively working against you: red and yellow squiggly lines everywhere, cryptic messages that seem to lead nowhere... and at some point you might start questioning whether switching to TypeScript was really a good idea, wondering if going back to plain JavaScript wouldn't be simpler.
Here's the thing though: bringing TypeScript into a React codebase is one of the smartest moves you can make if you care about maintaining quality code over time.
And those errors and warnings you keep bumping into? They're absolutely worth pushing through, you just have to learn to recognize the recurring patterns behind them.
Most TypeScript issues you hit while building React apps aren't random at all. The same handful of errors show up again and again across projects, and once you've dealt with them a few times, you'll start spotting them instantly and know exactly what they mean. This piece walks through five categories of errors that show up in virtually every TypeScript-powered React project, explaining why each one happens and how to resolve it.
One piece of advice before diving in: always read the entire error message. TypeScript's errors can look scary at a glance, but they actually follow a fairly predictable structure. Don't let the wall of text throw you off, and if you're ever unsure where to start, the final line of the message is usually the most useful part to focus on.
With that said, let's get into it.
1. Component props errors
Props sit at the core of every React component, so it makes sense that prop-related type errors are usually the first ones developers run into.
The error: Property 'X' does not exist on type '{}'.
This shows up when you use a prop in your component without ever giving it a type definition.
// This won't give any errors in JavaScript...
function UserCard({ name, email }) {
return (
<div>
<h3>{name}</h3>
<p>{email}</p>
</div>
);
};
// But in TypeScript...
// Error: Parameter 'name' implicitly has an 'any' type
// Error: Parameter 'email' implicitly has an 'any' type
This error appears simply because the props were never given explicit types.
The fix: declare an interface describing your props.
interface UserCardProps {
name: string;
email: string;
};
function UserCard({ name, email}: UserCardProps) {
return (
<div>
<h3>{name}</h3>
<p>{email}</p>
</div>
);
};
The error: Type 'string' is not assignable to type 'number'.
This one is fairly straightforward: it means you passed a prop with a value of the wrong type.
The fix: verify the type of whatever value you're passing into the prop.
interface ItemForSaleProps {
imgUrl: string;
itemName: string;
amount: number;
currency: string;
};
function ItemForSale({
itemName,
imgUrl,
amount,
currency
}: ItemForSaleProps) {
return (
<div class="item-for-sale">
<img src={imgUrl} alt="item image" />
<h5>{itemName}</h5>
<p>{`${currency} ${amount.toFixed(2)}`}</p>
</div>
);
};
// Error happens when passing a string where a number is expected.
<ItemForSale
imgUrl="api.example.com/image"
itemName="Sample"
amount="10.99"
currency="USD"
/>
// Should be like this
<ItemForSale
imgUrl="api.example.com/image"
itemName="Sample"
amount={10.99}
currency="USD"
/>
Notice the difference between the two versions? The ItemForSale component expects amount to be a number, so passing a string triggers an error, while passing an actual numeric value is correct. This is TypeScript doing exactly what it's meant to do, catching a potential bug before your code ever runs. In this example, calling .toFixed(2) on the string '10.99' would actually crash at runtime, but TypeScript flags the problem during compilation instead.
The error: Property 'X' is missing in type '{}' but required in type 'Props'.
This happens when a prop is marked as required in your interface, but you forget to actually pass it when using the component.
interface CustomButtonProps {
label: string;
onClick: () => void;
};
function CustomButton({ label, onClick }: CustomButtonProps) {
return <button onClick={onClick}>{label}</button>;
};
// Bad usage:
<CustomButton label="Submit" />
// Error: Property 'onClick' is missing in type '{ label: string; }'
// but required in type 'ButtonProps'
The fix: double-check both the prop definitions on your component and the props you're actually supplying when rendering it.
// Correct usage:
<CustomButton label="Submit" onClick={handleSubmit} />
Sometimes you'll have props that aren't always needed. In those cases, you can mark a prop as optional with a ?. Just keep in mind that whenever you make a prop optional, you should also provide either a default value or some kind of null check to handle its absence safely.
interface CustomButtonProps {
label: string;
onClick: () => void;
disabled?: boolean; // this prop is now optional
className?: string; // this one too
};
function CustomButton({ label, onClick, disabled = false, className }: CustomButtonProps) {
return (
<button
onClick={onClick}
disabled={disabled}
className={className}
>
{label}
</button>
);
};
// This component now works with or without the optional props
<CustomButton label="Submit" onClick={handleSubmit} />
<CustomButton label="Submit" onClick={handleSubmit} disabled={true} />
Now that optional props are in the picture, there's another error worth mentioning.
The error: Type 'X | undefined' is not assignable to type 'X'.
Making a prop optional automatically adds undefined to its type, which causes problems if you try to use that value without first checking whether it exists.
interface ProfileProps {
name: string;
bio?: string;
};
function Profile({ name, bio }: ProfileProps) {
return (
<p>{name}</p>
<p>{bio.toUpperCase()}</p>
// Error: Object is possibly 'undefined'
);
};
You generally have three ways to deal with this:
Option 1: provide a default value during props destructuring.
// bio is always a string, will render a default text when there's
// no bio provided
function Profile({ name, bio = 'No bio available' }: ProfileProps) {
return (
<p>{name}</p>
<p>{bio.toUpperCase()}</p>
);
};
Option 2: rely on optional chaining.
// returns undefined if bio is undefined
function Profile({ name, bio }: ProfileProps) {
return (
<p>{name}</p>
<p>{bio?.toUpperCase()}</p>
);
};
Option 3: use conditional rendering.
// will only render if bio exists
function Profile({ name, bio }: ProfileProps) {
return (
<p>{name}</p>
<p>{bio && bio.toUpperCase()}</p>
);
};
2. Event Handler errors
Any React component that responds to user interaction needs event handlers, and TypeScript comes with very precise types for each kind of DOM event. Getting these types wrong is one of the most frequent sources of confusion for developers working with typed React code.
The issue: Parameter 'e' implicitly has an 'any' type.
This shows up when you write an event handler as a standalone function outside your JSX and forget to annotate the event parameter.
// Error: Parameter 'e' implicitly has an 'any' type
const handleClick = (e) => {
e.preventDefault();
};
The fix: Attach the matching React event type to the parameter.
const handleClick = (e: React.MouseEvent<HTMLButtonElement>) => {
e.preventDefault();
};
Worth noting: when you write the handler directly inline inside your JSX, TypeScript can figure out the type on its own, so this particular error won't appear in that case.
<button onClick={(e) => {
e.preventDefault() // e is automatically React.MouseEvent<HTMLButtonElement>
}}>
Click here
</button>
The issue: Property 'value' does not exist on type 'EventTarget'.
This might be the single most searched TypeScript error among React developers — it's also one of the trickiest to understand the first time you hit it. It shows up when you try to read e.target.value inside a change handler.
// This will give an error because TS doesn't know target is an input element.
const handleChange = (e: React.ChangeEvent) => {
console.log(e.target.value);
// Error: Property 'value' does not exist on type 'EventTarget'
};
The root cause is that EventTarget is a generic DOM interface, so TypeScript has no way of knowing that, in this particular handler, the target is actually an <input> element that happens to expose a value property.
The fix: Supply the specific element type as a generic parameter to the event type.
// TypeScript now knows the target is an HTMLInputElement
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
console.log(e.target.value);
};
The same pattern works for other elements — for a select element, swap HTMLInputElement for HTMLSelectElement, for instance. Here's a quick reference for some of the event types you'll use most often:
// Click events
onClick: (e: React.MouseEvent<HTMLButtonElement>) => void
onClick: (e: React.MouseEvent<HTMLDivElement>) => void
// Input change events
onChange: (e: React.ChangeEvent<HTMLInputElement>) => void
onChange: (e: React.ChangeEvent<HTMLSelectElement>) => void
onChange: (e: React.ChangeEvent<HTMLTextAreaElement>) => void
// Form submission
onSubmit: (e: React.FormEvent<HTMLFormElement>) => void
// Keyboard events
onKeyDown: (e: React.KeyboardEvent<HTMLInputElement>) => void
onKeyUp: (e: React.KeyboardEvent<HTMLInputElement>) => void
// Focus events
onFocus: (e: React.FocusEvent<HTMLInputElement>) => void
onBlur: (e: React.FocusEvent<HTMLInputElement>) => void
Since we're on the topic of event handlers, you might wonder how to type them correctly when passing them down as props to child components. In that case, they should be typed as functions that accept the appropriate event object.
interface SearchInputProps {
onSearch: (e: React.ChangeEvent<HTMLInputElement>) => void;
onSubmit: (e: React.FormEvent<HTMLFormElement>) => void;
};
function SearchInput({ onSearch, onSubmit }: SearchInputProps) {
return (
<form onSubmit={onSubmit}>
<input type="text" onChange={onSearch} />
<button type="submit">Search</button>
</form>
);
};
And if a given handler doesn't need to receive the event at all, you can simplify its type accordingly.
interface SearchInputProps {
onClick: () => void;
onHover: () => void;
};
3. useState and useRef
Hooks sit at the center of modern React development, so it's no surprise that useState and useRef come with a few TypeScript quirks of their own.
The issue: Argument of type 'X' is not assignable to parameter of type 'never'.
This is one of the more baffling errors you can run into. It happens when you initialize useState with an empty array, causing TypeScript to infer the state's type as never[]. Here's what that looks like:
// TypeScript infers state type as never[]
const [items, setItems] = useState([]);
// So later when we try to set a value...
setItems([{ id: 1, name: 'Item 1' }]);
// Error: Argument of type '{ id: number; name: string; }[]' is not assignable
// to parameter of type 'never[]'
The reasoning behind it: when the initial value of your state is an empty array, TypeScript has no way to guess what kind of elements will eventually live inside it, so it defaults to the never[] type.
The fix: Always give an explicit type argument when your initial state is an empty array or null.
interface Item {
id: number;
name: string;
}
// ✅ Explicitly typed
const [items, setItems] = useState<Item[]>([]);
const [selectedItem, setSelectedItem] = useState<Item | null>(null);
In that snippet, the selectedItem state is explicitly typed to accept either an Item object or null. Whenever you expect a piece of state to start out empty and be filled in later, you need to spell that out for TypeScript — otherwise you'll run into this error:
Type 'null' is not assignable to type 'X'.
interface User {
id: number;
name: string;
email: string;
}
// TypeScript infers state as User, not User | null
const [user, setUser] = useState<User>({} as User); // Dangerous cast
// Later...
if (user.name) { /* ... */ }
// This might not catch the case where user is empty
The fix: Use a union type that includes null so TypeScript understands the data hasn't loaded yet.
// Correctly typed, can be type User or null
const [user, setUser] = useState<User | null>(null);
// Now TypeScript forces you to handle the null case
if (user) {
console.log(user.name); // TypeScript knows user is User here
}
// Or with optional chaining:
console.log(user?.name); // string | undefined
The same idea applies when your state holds a more complex object. Say you're modeling the state for a contact form with several fields — the right approach is to first define an interface describing that shape.
interface FormState {
name: string;
email: string;
message: string;
isSubmitting: boolean;
}
function SubmitMessageForm() {
const [form, setForm] = useState<FormState>({
name: '',
email: '',
message: '',
isSubmitting: false,
});
// Then do partial updates with spread operator
const handleChange = (field: keyof FormState, value: string) => {
setForm(prevState => ({ ...prevState, [field]: value }));
};
}
The issue: Object is possibly 'null'.
This one shows up constantly when a ref created with useRef starts out as null and is meant to point to a DOM element. TypeScript will flag any attempt to use that ref before confirming it actually holds something.
const inputRef = useRef<HTMLInputElement>(null);
// Typescript here knows that inputRef.current might be null
inputRef.current.focus();
// Error: Object is possibly 'null'
The solution: verify that current is set before you use it. Once TypeScript sees that guard, it stops complaining because it can no longer prove the value might be null.
// Null check before use
const handleFocus = () => {
if (inputRef.current) {
inputRef.current.focus();
}
};
// Or if you're more into one-liners, you can use optional chaining
inputRef.current?.focus();
Before moving to the next category, there's an important change worth calling out. React 19 altered how useRef behaves in a way that trips up a lot of developers — specifically the distinction between RefObject<T|null> and MutableRefObject<T>. What determines the behavior now isn't the initial value you pass in, but the generic type parameter you give the hook.
Prior to React 19, calling useRef(null) always returned a MutableRefObject<T>, so current could always be reassigned.
// This worked fine
const ref = useRef<HTMLInputElement | null>(null);
ref.current = someElement;
Starting with React 19, the generic type you specify determines whether current is writable.
const ref = useRef<HTMLInputElement>(null);
ref.current = someElement;
// The above will result in an error.
Here, because the generic parameter is an HTML element type, React 19 treats the ref as one that points to the DOM, so it becomes read-only and effectively managed by React itself.
If instead you need a ref for storing a mutable value rather than a DOM node, do this:
// Let's suppose we're building a timer
const ref = useRef<ReturnType<typeof setTimeout> | null>(null);
ref.current = setTimeout(() => {}, 1000);
This works because the type passed to the generic isn't an HTML element type, so React treats it as a plain mutable ref — current remains assignable.
The rule for React 19: when the generic type is an HTML element type, current becomes read-only and is controlled by React. When it's any other type, current stays writable. In short:
useRef<HTMLElement>(null)for DOM nodes managed by React (read-only)useRef<T|null>(null)for any other mutable value you want to store yourself
4. Async Data and API Errors
Once you start pulling data from an API, a fresh set of TypeScript errors appears, because fetched data begins life as unknown and has to pass through several transformations before it becomes something you can safely use.
The error: Type 'unknown' is not assignable to type 'X'.
By default, the result of a fetch call is typed as unknown, since TypeScript has no built-in knowledge of what shape a given endpoint returns.
// Fetch response is unknown
const response = await fetch('/api/users');
const data = await response.json(); // data: any (in older TS) or unknown
// So when trying to use the fetched data:
const userName = data.name;
// With the code above, you might get a warning, something like
// 'data' is of type 'unknown'
The fix: declare an explicit type for your API response.
interface User {
id: number;
name: string;
email: string;
};
From there, you generally have two options for applying that type:
// Option 1: Type assertion. Only use this when you trust the API shape.
const response = await fetch('/api/users/1');
const data = await response.json() as User;
console.log(data.name); // You'll see that you have a string here
//////////////////////////////////////////////////////////////////////////
// Option 2: Create a generic fetch wrapper, this is a safer approach
// and it's reusable.
async function fetchJSON<T>(url: string): Promise<T> {
const response = await fetch(url);
if (!response.ok) {
throw new Error(`HTTP error: ${response.status}`);
}
return response.json() as Promise<T>;
}
const user = await fetchJSON<User>('/api/users/1');
console.log(user.name); // TypeScript now knows this is a string
5. Children and JSX Errors
Components in React routinely accept and render children, and TypeScript offers a few overlapping types to describe what children can be. Knowing how they differ helps you avoid a whole family of type errors.
Three types tend to get used as if they were interchangeable, even though they aren't:
- ReactNode
- ReactElement
- JSX.Element
Of these, ReactNode and ReactElement are genuinely distinct, while JSX.Element is really just another name for ReactElement.
import { ReactNode, ReactElement } from 'react';
// ReactNode is the most permissive one, accepts basically
// everything React can render:
// string, number, boolean, null, undefined, ReactElement, arrays...
type ReactNode =
| ReactElement
| string
| number
| boolean
| null
| undefined
| ReactPortal
| Iterable<ReactNode>;
// ReactElement is literally a React element created by
// JSX or React.createElement
// It does not include string, number, null, undefined
type ReactElement = {
type: string | ComponentType;
props: any;
key: string | null;
};
As a rule of thumb: default to ReactNode for your children prop unless you have a concrete reason to narrow it down.
// Most flexible: use ReactNode for children in most cases
interface CardProps {
children: ReactNode;
};
// And use ReactElement when the requirements are not very flexible
interface StrictWrapperProps {
children: ReactElement;
};
It's worth spelling out the difference between ReactNode and ReactElement more precisely.
A ReactNode can look like any of these:
const example1 = <div>Hello</div>; // ReactElement ✅
const example2 = "Hello"; // string ✅
const example3 = 42; // number ✅
const example4 = true; // boolean ✅ (renders nothing)
const example5 = null; // null ✅ (renders nothing)
const example6 = undefined; // undefined ✅ (renders nothing)
const example7 = [<div />, <span />]; // array of ReactElements ✅
// All of the above are ReactNode
A ReactElement, on the other hand, is more constrained:
const example1 = <div>Hello</div>;
const example2 = <Button onClick={someHandlerFn}>Submit</Button>;
const example3 = React.createElement('div', null, 'Hello');
// Under the hood, every ReactElements look like this:
{
type: 'div',
props: { children: 'Hello' },
key: null,
}
The error: Type 'X' is not assignable to type 'ReactNode'.
This typically comes up when you try to render a value that TypeScript doesn't recognize as valid renderable content.
// Objects are not a valid React children
interface User {
name: string;
email: string;
}
// Then if you try to do this
function DisplayUser({ user }: { user: User }) {
return <div>{user}</div>;
}
// It'll probably give you an error that looks something like...
// Error: Type 'User' is not assignable to type 'ReactNode'
// Objects are not valid React Children
The fix: render individual fields instead of the whole object.
function DisplayUser({ user }: { user: User }) {
return (
<div>
<p>{user.name}</p>
<p>{user.email}</p>
</div>
);
}
The error: JSX element type 'X' does not have any construct or call signatures.
This one can be confusing at first. It shows up when you pass a value somewhere expecting it to behave as a component, but TypeScript has no way to confirm that it actually is one.
// TypeScript doesn't know 'icon' is a valid component
interface ButtonProps {
icon: object; // too vague
};
// So when you try this...
function Button({ icon: Icon }: ButtonProps) {
return <Icon />;
}
// The above code will give you the error
// Error: JSX element type 'Icon' does not have any construct
// or call signatures
The fix: type dynamic components with React.ComponentType:
import { ComponentType } from 'react';
interface ButtonProps {
icon: ComponentType;
};
function Button({ icon: Icon }: ButtonProps) {
return (
<button>
<Icon />
</button>
);
}
// TypeScript now knows Icon is a valid component
If that component also takes props, you can describe them explicitly:
interface IconProps {
size?: number;
color?: string;
};
interface ButtonProps {
icon: ComponentType<IconProps>;
};
function Button({ icon: Icon }: ButtonProps) {
return (
<button>
<Icon size={12} color="white" />
</button>
);
}
// So now the props are typed too
A handy shortcut worth knowing is React's built-in PropsWithChildren utility type. It automatically adds children?: ReactNode to whatever props interface you wrap it around, saving you from redeclaring that field every time:
import { PropsWithChildren } from 'react';
// Now instead of doing this
interface CardProps {
title: string;
children?: ReactNode;
};
// You can do this
type CardProps = PropsWithChildren<{ title: string }>;
// So with this, instead of explicitly typing children manually,
// you can use the above code
function Card({ title, children }: CardProps) {
return (
<div>
<h1>{title}</h1>
<div>{children}</div>
</div>
);
}
The issue: rendering lists of elements
TypeScript enforces strict rules around keys in rendered lists, but most of the type errors you'll hit here actually stem from how the elements themselves are typed, not the keys.
// Items might be undefined, or item.id might not be a valid key type
function ItemList({ items }: { items: Item[] | undefined }) {
return (
<ul>
{
items.map(item => (
<li key={item.id}>{item.name}</li>
))
}
</ul>
);
}
// Error: Object is possibly 'undefined'
The fix: guard against undefined values and make sure the typing lines up correctly:
function ItemList({ items = [] }: { items?: Item[] }) {
return (
<ul>
{
items.map((item) => (
<li key={item.id}>{item.name}</li>
))
}
</ul>
);
}
To finish…
TypeScript errors stop feeling like roadblocks once you start treating them as clues. That shift happens the moment you stop reacting with "ugh, not this again" and instead start asking "what is TypeScript actually trying to tell me here?"
The errors covered in this guide are ones you'll keep running into throughout your work with React and TypeScript. What separates a developer who constantly struggles against the type system from one who works comfortably with it usually comes down to pattern recognition, nothing more. At this point, you've got the patterns.