You copied an event handler from JavaScript to TypeScript. Red squiggly lines everywhere. The error message is incomprehensible. You try a few things. Nothing works. You google. Stack Overflow suggests as any. It works, but you feel dirty.
I've been there. We've all been there.
TypeScript in React can feel like fighting the compiler instead of the bugs. The type system that's supposed to help you seems to actively work against you. Simple things in JavaScript become battles in TypeScript.
But here's the thing: most TypeScript frustrations in React have simple solutions. You just need to know the patterns. This is your field guide to those patterns—organized by the error messages that drive us crazy.
Frustration 1: "Type 'X' is not assignable to parameter of type 'Y'"
This is the granddaddy of TypeScript frustrations. You'll see it everywhere.
The Scenario: Event Handlers
// ❌ This drives people crazy
const handleClick = (event) => {
// Parameter 'event' implicitly has an 'any' type
console.log(event.target.value);
};
<button onClick={handleClick}>Click me</button>
The Error:
Parameter 'event' implicitly has an 'any' type.
Why it happens: TypeScript doesn't know what type event should be.
The Solution:
// ✅ Tell TypeScript the event type
const handleClick = (event: React.MouseEvent<HTMLButtonElement>) => {
console.log(event.currentTarget);
};
The Pattern:
React.[EventType]<HTML[ElementType]>
Quick Reference:
// Clicks
onClick={(e: React.MouseEvent<HTMLButtonElement>) => {}}
// Input changes
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {}}
// Form submits
onSubmit={(e: React.FormEvent<HTMLFormElement>) => {}}
// Keyboard
onKeyDown={(e: React.KeyboardEvent<HTMLInputElement>) => {}}
// Focus
onFocus={(e: React.FocusEvent<HTMLInputElement>) => {}}
The Even Better Solution
Let TypeScript infer when you can:
// ✅ Inline - type is inferred!
<button onClick={(e) => console.log(e.currentTarget)}>
Click me
</button>
// ✅ Only type when extracting the function
const handleClick = (e: React.MouseEvent<HTMLButtonElement>) => {
console.log(e.currentTarget);
};
Frustration 2: "Object is possibly 'null'"
This one makes developers want to throw their keyboards.
The Scenario: Refs
const inputRef = useRef<HTMLInputElement>(null);
// ❌ Object is possibly 'null'
inputRef.current.focus();
The Error:
Object is possibly 'null'.
Why it happens: Refs start as null before the component mounts. TypeScript is protecting you from a runtime error.
The Solutions:
// ✅ Option 1: Optional chaining (cleanest)
inputRef.current?.focus();
// ✅ Option 2: Check explicitly
if (inputRef.current) {
inputRef.current.focus();
}
// ✅ Option 3: Non-null assertion (use sparingly!)
// Only if you KNOW it's mounted
inputRef.current!.focus();
// ❌ Don't do this
(inputRef.current as any).focus();
When to use each:
- Optional chaining: Default choice, handles null gracefully
- Explicit check: When you need to do something else if null
- Non-null assertion: In useEffect or event handlers where you know it's mounted
- Type assertion to any: Never (defeats the purpose of TypeScript)
The Scenario: State
const [user, setUser] = useState<User | null>(null);
// ❌ Object is possibly 'null'
return <div>{user.name}</div>;
The Solution:
// ✅ Option 1: Early return
if (!user) return <div>Loading...</div>;
return <div>{user.name}</div>;
// ✅ Option 2: Optional chaining with fallback
return <div>{user?.name ?? 'Guest'}</div>;
// ✅ Option 3: Conditional rendering
return <div>{user ? user.name : 'Loading...'}</div>;
Frustration 3: "Type 'string' is not assignable to type 'never'"
This cryptic error often appears with arrays and state.
The Scenario: Empty Array State
// ❌ TypeScript thinks this is never[]
const [items, setItems] = useState([]);
// Later...
setItems(['item1', 'item2']);
// Type 'string' is not assignable to type 'never'
Why it happens: TypeScript infers never[] from an empty array (an array that can never have items).
The Solution:
// ✅ Provide the type explicitly
const [items, setItems] = useState<string[]>([]);
// ✅ Or with objects
interface Todo {
id: string;
text: string;
}
const [todos, setTodos] = useState<Todo[]>([]);
The Pattern: Always type empty array state explicitly.
Frustration 4: "Property 'X' does not exist on type 'Y'"
This usually happens when spreading props or working with events.
The Scenario: Event.target.value
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
// ❌ Property 'files' does not exist on type 'HTMLInputElement'
console.log(e.target.files);
};
Why it happens: You typed it as HTMLInputElement but it's actually a file input.
The Solution:
// ✅ Use the right element type
const handleFileChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const files = e.currentTarget.files; // Use currentTarget for type safety
if (files) {
console.log(files[0]);
}
};
// ✅ Or type assert if you know the element type
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const target = e.target as HTMLInputElement;
if (target.type === 'file') {
console.log(target.files);
}
};
The Scenario: Spreading Props
interface ButtonProps {
label: string;
onClick: () => void;
}
function MyButton(props: ButtonProps) {
// ❌ Property 'className' does not exist on type 'ButtonProps'
return <button {...props} />;
}
// Usage
<MyButton label="Click" onClick={handleClick} className="btn" />
The Solution:
// ✅ Extend the HTML button attributes
interface ButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
label: string;
}
function MyButton({ label, ...props }: ButtonProps) {
return <button {...props}>{label}</button>;
}
// Now this works!
<MyButton label="Click" onClick={handleClick} className="btn" disabled />
Common extends:
React.ButtonHTMLAttributes<HTMLButtonElement>
React.InputHTMLAttributes<HTMLInputElement>
React.HTMLAttributes<HTMLDivElement>
React.AnchorHTMLAttributes<HTMLAnchorElement>
Frustration 5: "No overload matches this call"
This cryptic error often appears with generics.
The Scenario: Generic Components
interface ListProps<T> {
items: T[];
renderItem: (item: T) => React.ReactNode;
}
// ❌ No overload matches this call
function List<T>(props: ListProps<T>) {
return <div>{props.items.map(props.renderItem)}</div>;
}
Why it happens: TypeScript struggles with generic function components.
The Solution:
// ✅ Option 1: Type the function properly
function List<T>({ items, renderItem }: ListProps<T>): React.ReactElement {
return <div>{items.map(renderItem)}</div>;
}
// ✅ Option 2: Use React.FC (though it's fallen out of favor)
const List: <T>(props: ListProps<T>) => React.ReactElement = ({ items, renderItem }) => {
return <div>{items.map(renderItem)}</div>;
};
// ✅ Option 3: Arrow function with explicit type
const List = <T,>({ items, renderItem }: ListProps<T>) => {
return <div>{items.map(renderItem)}</div>;
};
// Note the comma after T - prevents JSX parsing issues
Usage:
<List
items={users}
renderItem={(user) => <div>{user.name}</div>}
/>
Frustration 6: "Argument of type 'X' is not assignable to parameter of type 'SetStateAction'"
This happens when updating state with a complex object.
The Scenario: Updating Nested State
interface User {
name: string;
email: string;
address: {
street: string;
city: string;
};
}
const [user, setUser] = useState<User>(/* ... */);
// ❌ Type error nightmare
setUser({
name: 'John',
email: 'john@example.com'
// Missing 'address'
});
The Solution:
// ✅ Option 1: Spread the previous state
setUser(prev => ({
...prev,
name: 'John'
}));
// ✅ Option 2: Spread nested objects carefully
setUser(prev => ({
...prev,
address: {
...prev.address,
city: 'New York'
}
}));
// ✅ Option 3: Use immer for complex updates
import { produce } from 'immer';
setUser(prev => produce(prev, draft => {
draft.address.city = 'New York';
}));
Frustration 7: "Cannot invoke an object which is possibly 'undefined'"
This happens constantly with optional props and callbacks.
The Scenario: Optional Callbacks
interface Props {
onClick?: () => void;
}
function Button({ onClick }: Props) {
// ❌ Cannot invoke an object which is possibly 'undefined'
return <button onClick={onClick}>Click</button>;
}
The Solution:
// ✅ Option 1: Optional chaining
return <button onClick={() => onClick?.()}>Click</button>;
// ✅ Option 2: Guard clause
const handleClick = () => {
if (onClick) {
onClick();
}
};
return <button onClick={handleClick}>Click</button>;
// ✅ Option 3: Default value
function Button({ onClick = () => {} }: Props) {
return <button onClick={onClick}>Click</button>;
}
Frustration 8: ForwardRef and useImperativeHandle Hell
ForwardRef is notoriously difficult to type correctly.
The Scenario: ForwardRef
// ❌ This looks right but has issues
const Input = forwardRef((props, ref) => {
return <input ref={ref} {...props} />;
});
The Solution:
// ✅ Type both props and ref
interface InputProps {
placeholder?: string;
}
const Input = forwardRef<HTMLInputElement, InputProps>((props, ref) => {
return <input ref={ref} {...props} />;
});
// Usage
const inputRef = useRef<HTMLInputElement>(null);
<Input ref={inputRef} placeholder="Enter text" />
The Scenario: useImperativeHandle
interface InputMethods {
focus: () => void;
clear: () => void;
}
const Input = forwardRef<InputMethods, InputProps>((props, ref) => {
const inputRef = useRef<HTMLInputElement>(null);
useImperativeHandle(ref, () => ({
focus: () => {
inputRef.current?.focus();
},
clear: () => {
if (inputRef.current) {
inputRef.current.value = '';
}
}
}));
return <input ref={inputRef} {...props} />;
});
// Usage
const inputRef = useRef<InputMethods>(null);
inputRef.current?.focus(); // ✅ TypeScript knows these methods exist
inputRef.current?.clear();
Frustration 9: "Type 'ReactNode' is not assignable to type 'ReactElement'"
This happens when mixing different children types.
The Scenario: Restrictive Children
interface LayoutProps {
children: React.ReactElement;
}
function Layout({ children }: LayoutProps) {
return <div className="layout">{children}</div>;
}
// ❌ Error: Type 'string' is not assignable to type 'ReactElement'
<Layout>Hello World</Layout>
// ❌ Error: Type 'Element[]' is not assignable to type 'ReactElement'
<Layout>
<div>One</div>
<div>Two</div>
</Layout>
The Solution:
// ✅ Use ReactNode for any renderable content
interface LayoutProps {
children: React.ReactNode;
}
// ✅ Use ReactElement only when you truly need a single element
interface WrapperProps {
children: React.ReactElement;
}
// ✅ Use ReactElement[] for multiple elements
interface TabsProps {
children: React.ReactElement[];
}
// ✅ Or be very specific
interface TabsProps {
children: React.ReactElement<TabProps> | React.ReactElement<TabProps>[];
}
Frustration 10: "Expression produces a union type that is too complex to represent"
This monster appears with complex state or reducers.
The Scenario: Complex Reducer Actions
// ❌ Gets too complex quickly
type Action =
| { type: 'SET_USER'; payload: User }
| { type: 'SET_POSTS'; payload: Post[] }
| { type: 'SET_COMMENTS'; payload: Comment[] }
// ... 20 more actions
The Solution:
// ✅ Split into smaller action groups
type UserAction =
| { type: 'USER_LOGIN'; payload: User }
| { type: 'USER_LOGOUT' };
type PostAction =
| { type: 'POST_CREATE'; payload: Post }
| { type: 'POST_DELETE'; payload: string };
type Action = UserAction | PostAction;
// ✅ Or use discriminated unions with a common field
type Action = {
domain: 'user' | 'post' | 'comment';
type: string;
payload?: any;
};
Frustration 11: Third-Party Libraries Without Types
This is common with older or poorly maintained libraries.
The Scenario: No Type Definitions
// ❌ Could not find a declaration file for module 'old-library'
import something from 'old-library';
The Solutions:
// ✅ Option 1: Check DefinitelyTyped
npm install --save-dev @types/old-library
// ✅ Option 2: Create a declaration file
// Create: src/types/old-library.d.ts
declare module 'old-library' {
export function something(param: string): void;
}
// ✅ Option 3: Quick and dirty (for prototyping only)
declare module 'old-library';
// Now you can import, but no type safety
// ✅ Option 4: Type specific imports
// src/types/old-library.d.ts
declare module 'old-library' {
const library: any;
export default library;
}
Frustration 12: "Type instantiation is excessively deep and possibly infinite"
This cryptic error appears with deeply nested types.
The Scenario: Recursive Components
interface TreeNode {
value: string;
children: TreeNode[];
}
// ❌ Can cause "excessively deep" errors with deep trees
interface TreeProps {
node: TreeNode;
}
The Solution:
// ✅ Add a depth limit or simplify the type
interface TreeNode {
value: string;
children?: TreeNode[]; // Make optional
}
// ✅ Or use unknown for very deep structures
interface TreeNode {
value: string;
children?: unknown; // Type more loosely
}
// ✅ Or limit recursion depth
type TreeNode = {
value: string;
children?: Array<{
value: string;
children?: Array<{
value: string;
// Stop here
}>;
}>;
};
The Escape Hatches (Use Wisely)
Sometimes you need to tell TypeScript "trust me."
The any Escape Hatch
// ❌ Never do this everywhere
const handleChange = (e: any) => {
e.target.value; // No safety at all
};
// ✅ Use any surgically for truly dynamic code
const processData = (data: any) => {
// External API with unknown shape
return data;
};
// ✅ Better: use unknown and narrow
const processData = (data: unknown) => {
if (typeof data === 'object' && data !== null) {
// Type narrowing
}
};
The as Assertion
// ❌ Don't lie to TypeScript
const value = someValue as CompletelyDifferentType;
// ✅ Use for widening or narrowing you're certain about
const input = e.target as HTMLInputElement;
const value = response.data as User;
// ✅ Better: use type guards
function isUser(data: unknown): data is User {
return typeof data === 'object' && data !== null && 'name' in data;
}
if (isUser(response.data)) {
// TypeScript knows it's User here
}
The Non-Null Assertion
// ❌ Don't use when you're not sure
const value = maybeNull!.property;
// ✅ Use only when you have proof
useEffect(() => {
// Inside useEffect, ref is guaranteed to be set
inputRef.current!.focus();
}, []);
// ✅ Better: handle the null case
inputRef.current?.focus();
Debugging TypeScript Errors
When you see a massive error, here's how to decode it:
1. Start at the End
Type 'X' is not assignable to type 'Y'.
Type 'A' is not assignable to type 'B'.
Type 'C' is not assignable to type 'D'.
Type 'string' is not assignable to type 'number'. <-- Start here
The last line usually tells you the actual problem.
2. Hover Over Everything
Hover over variables in VSCode to see what TypeScript thinks the type is. Often reveals mismatches.
3. Simplify
Comment out code until the error goes away, then add back piece by piece.
4. Check the Documentation
React type definitions are well-documented. Ctrl/Cmd + Click on types to see their definitions.
5. Use Type Utilities
// See what TypeScript infers
type WhatIsThis = typeof myVariable;
// Extract types
type PropsType = React.ComponentProps<typeof MyComponent>;
type ReturnType = ReturnType<typeof myFunction>;
The "Why Does This Work?" Examples
Sometimes TypeScript just works, and you're not sure why. Here's the magic:
Type Inference in Event Handlers
// TypeScript knows e is React.MouseEvent<HTMLButtonElement>
<button onClick={(e) => console.log(e.currentTarget)}>
Click
</button>
Why: The onClick prop is typed in React's button definition, so TypeScript infers the event type.
Destructuring Props
function Button({ onClick, children }: ButtonProps) {
// TypeScript knows the types of onClick and children
}
Why: Destructuring maintains types from the interface.
Return Type Inference
function useCounter(initial: number) {
const [count, setCount] = useState(initial);
return { count, setCount }; // TypeScript infers the return type
}
Why: TypeScript can infer complex return types from the implementation.
Quick Fixes Cheat Sheet
// ❌ Problem → ✅ Solution
// Parameter implicitly has 'any' type
const handler = (e) => {}
const handler = (e: React.ChangeEvent<HTMLInputElement>) => {}
// Object is possibly 'null'
ref.current.focus()
ref.current?.focus()
// Type 'never[]' is not assignable
const [items, setItems] = useState([])
const [items, setItems] = useState<string[]>([])
// Property 'X' does not exist
interface Props { label: string }
interface Props extends React.ButtonHTMLAttributes<HTMLButtonElement> { label: string }
// Cannot invoke possibly 'undefined'
<button onClick={onClick}>
<button onClick={() => onClick?.()}>
// Type 'string' is not assignable to 'ReactElement'
children: React.ReactElement
children: React.ReactNode
// Could not find declaration file
import 'library'
npm install @types/library OR create library.d.ts
// Excessive depth
type Deep = { children: Deep[] }
type Deep = { children?: unknown }
When to Stop Fighting
Sometimes TypeScript is telling you something important:
Listen to TypeScript when:
- It catches actual bugs (null references, wrong types)
- It prevents runtime errors
- It enforces your own type definitions
Push back on TypeScript when:
- The types don't match reality (external APIs)
- You're writing throw-away prototype code
- The complexity outweighs the benefit
The Golden Rules
- Let TypeScript infer when it can - Don't over-annotate
- Type at boundaries - Props, API responses, state initialization
- Use unknown over any - Force yourself to narrow types
- Read the error from bottom to top - The real problem is usually at the end
- Trust the compiler - If it's hard to type, maybe the design needs work
- Use escape hatches sparingly - They're for last resort, not first choice
Conclusion
TypeScript frustration in React usually comes from not knowing the patterns. Once you've seen the pattern, the error transforms from "WHAT DOES THIS MEAN?!" to "Oh right, I need to type that."
Keep this guide bookmarked. Next time you see Type 'X' is not assignable to type 'Y', come back here. Find your error. Copy the solution. Move on with your life.
TypeScript in React should make you more productive, not less. These patterns are your shortcuts to that productivity.
Stop fighting. Start flowing.
What TypeScript error drives you the craziest? Share in the comments—I'll add solutions for the most common ones.
Top comments (0)