TypeScript for React Developers: 12 Common Mistakes and Best Practices

Master TypeScript React best practices by avoiding these 12 common mistakes. Learn proper component typing, hooks patterns, and API integration techniques.
Author
GreatFrontEnd Team
26 min read
Dec 8, 2025
TypeScript for React Developers: 12 Common Mistakes and Best Practices

Remember the first time you added TypeScript to a React project? The initial excitement of autocomplete and type safety quickly turned into frustration with cryptic error messages and confusing type definitions. You're not alone - this is the reality for most developers making the switch.

TypeScript with React has become the industry standard. React 19, Next.js 16, and virtually every modern framework now ship with first-class TypeScript support. Companies expect it, teams rely on it, and your career growth depends on mastering it.

But here's the problem: most developers learn TypeScript reactively, fixing errors as they appear rather than understanding the patterns that prevent them. This leads to codebases filled with any types, overly complex generics, and brittle component APIs that break during refactoring.

The cost is real. A single poorly-typed component can cascade into hours of debugging. Missing event handler types lead to runtime crashes. Incorrect hook typing creates subtle bugs that only appear in production.

This post reveals the 12 most common TypeScript mistakes React developers make and shows you exactly how to fix them. You'll learn the patterns senior developers use, understand why certain approaches work better than others, and gain the confidence to write type-safe React code from day one.

Ready to level up your TypeScript skills? Let's dive in. And when you're done, head over to GreatFrontEnd's TypeScript interview questions to practice and prepare for your next interview.

Why TypeScript matters for frontend developers?

TypeScript isn't just about catching bugs - it's about building better software faster.

Type safety means catching errors before they reach production. Instead of discovering that user.profile.avatar is undefined at 2 AM when your app crashes, TypeScript tells you at compile time. No more defensive coding with endless null checks.

Productivity gains are immediate and measurable. IntelliSense shows you every available prop as you type. Autocomplete writes half your code for you. Refactoring a component name? TypeScript updates every import automatically. What used to take hours now takes minutes.

Collaboration improves dramatically. Your component's props are self-documenting. New team members understand your API without reading documentation. Code reviews focus on logic, not "what does this parameter do?"

Hiring trends are clear: 78% of React job postings now require TypeScript experience. It's no longer optional - it's expected.

Before and After: Type safety in action

// ❌ Without TypeScript - Runtime error waiting to happen
function UserProfile({ user }) {
return <div>{user.profile.name}</div>;
}
// ✅ With TypeScript - Error caught at compile time
type User = {
profile?: {
name: string;
};
};
function UserProfile({ user }: { user: User }) {
return <div>{user.profile?.name ?? 'Anonymous'}</div>;
}

Did you know? Teams using TypeScript report 15% fewer production bugs and 20% faster onboarding for new developers.


Component typing mistakes

Mistake 1: Using React.FC incorrectly

React.FC seems convenient - it types children automatically and provides type inference. But it comes with hidden costs that experienced teams avoid.

What React.FC does:

  • Automatically includes children in props (even when you don't want it)
  • Provides implicit return type
  • Adds displayName, propTypes, and other legacy properties

Why explicit typing is clearer:

// ❌ Using React.FC - children included even when not needed
const Button: React.FC<{ onClick: () => void }> = ({ onClick }) => {
return <button onClick={onClick}>Click me</button>;
};
// This compiles but shouldn't - Button doesn't render children!
<Button onClick={handleClick}>
<span>This text is ignored</span>
</Button>;
// ✅ Explicit prop typing - clear and intentional
type ButtonProps = {
onClick: () => void;
};
function Button({ onClick }: ButtonProps) {
return <button onClick={onClick}>Click me</button>;
}
// ❌ TypeScript error - children not accepted
<Button onClick={handleClick}>
<span>Error!</span>
</Button>;

When teams avoid React.FC:

  • When components don't accept children
  • When you need precise control over prop types
  • In modern codebases (React 18+)

Mistake 2: Not typing component props properly

Using any for props defeats the entire purpose of TypeScript. Your components become black boxes, autocomplete disappears, and refactoring becomes dangerous.

Why any breaks correctness:

// ❌ Props typed as any - no safety, no autocomplete
function Card({ title, description, variant }: any) {
return (
<div className={variant}>
<h2>{title}</h2>
<p>{description}</p>
</div>
);
}
// This compiles but crashes at runtime
<Card variant={123} />;

Proper typing with optional props and variants:

// ✅ Explicit prop types with variants
type CardProps = {
title: string;
description?: string; // Optional prop
variant?: 'primary' | 'secondary' | 'danger'; // Union type for variants
onClose?: () => void;
};
function Card({ title, description, variant = 'primary', onClose }: CardProps) {
return (
<div className={`card card--${variant}`}>
<h2>{title}</h2>
{description && <p>{description}</p>}
{onClose && <button onClick={onClose}>×</button>}
</div>
);
}
// ✅ TypeScript catches errors
<Card title="Hello" variant="invalid" />; // Error: Type '"invalid"' is not assignable

Real-world design system example:

type TagProps = {
label: string;
size?: 'sm' | 'md' | 'lg';
color?: 'blue' | 'green' | 'red' | 'gray';
removable?: boolean;
onRemove?: () => void;
};
function Tag({
label,
size = 'md',
color = 'gray',
removable = false,
onRemove,
}: TagProps) {
return (
<span className={`tag tag--${size} tag--${color}`}>
{label}
{removable && <button onClick={onRemove}>×</button>}
</span>
);
}

Key takeaway: Always type your TypeScript React components explicitly. Mark optional props with ? and use union types for variants.


Mistake 3: Incorrect event handler typing

Event handlers are one of the most commonly mistyped patterns in React. Using Function or any seems harmless until you need to access event.target.value and TypeScript can't help you.

Common misuse:

// ❌ Too loose - no type safety
function SearchInput({ onChange }: { onChange: Function }) {
return <input onChange={onChange} />;
}
// ❌ Using any - defeats the purpose
function SearchInput({ onChange }: { onChange: any }) {
return <input onChange={onChange} />;
}

Correct event type patterns:

// ✅ Proper event typing
type SearchInputProps = {
onChange: (event: React.ChangeEvent<HTMLInputElement>) => void;
};
function SearchInput({ onChange }: SearchInputProps) {
return <input type="text" onChange={onChange} />;
}
// Usage with full type safety
function App() {
const handleSearch = (event: React.ChangeEvent<HTMLInputElement>) => {
console.log(event.target.value); // ✅ TypeScript knows this is a string
};
return <SearchInput onChange={handleSearch} />;
}

Event types mapped to elements:

Element typeEvent typeCommon use case
<input>, <textarea>React.ChangeEvent<HTMLInputElement>Form inputs
<form>React.FormEvent<HTMLFormElement>Form submission
<button>, <div>React.MouseEvent<HTMLButtonElement>Click handlers
<input>React.KeyboardEvent<HTMLInputElement>Keyboard shortcuts
<input>React.FocusEvent<HTMLInputElement>Focus/blur events

Real-world form validation scenario:

type LoginFormProps = {
onSubmit: (email: string, password: string) => void;
};
function LoginForm({ onSubmit }: LoginFormProps) {
const handleSubmit = (event: React.FormEvent<HTMLFormElement>) => {
event.preventDefault();
const formData = new FormData(event.currentTarget);
const email = formData.get('email') as string;
const password = formData.get('password') as string;
onSubmit(email, password);
};
return (
<form onSubmit={handleSubmit}>
<input name="email" type="email" required />
<input name="password" type="password" required />
<button type="submit">Login</button>
</form>
);
}

Mistake 4: forwardRef typing confusion

forwardRef is notoriously tricky to type correctly. The generic syntax is confusing, and combining it with custom props often leads to type errors that are hard to debug.

Why forwardRef is tricky:

// ❌ Common mistake - ref type is wrong
const Input = forwardRef((props, ref) => {
return <input ref={ref} {...props} />;
});
// Error: Type 'ForwardedRef<unknown>' is not assignable to type 'LegacyRef<HTMLInputElement>'

Correct generic syntax:

// ✅ Proper forwardRef typing
type InputProps = {
placeholder?: string;
error?: boolean;
};
const Input = forwardRef<HTMLInputElement, InputProps>(
({ placeholder, error }, ref) => {
return (
<input
ref={ref}
placeholder={placeholder}
className={error ? 'input--error' : 'input'}
/>
);
},
);
Input.displayName = 'Input';

Common error messages and fixes:

Error messageFix
Type 'ForwardedRef<unknown>' is not assignableAdd generic types: forwardRef<ElementType, PropsType>
Property 'displayName' does not existAdd ComponentName.displayName = 'Name' after definition
Type instantiation is excessively deepSimplify prop spreading or use ComponentPropsWithoutRef

Hooks typing mistakes

Mistake 5: Not typing useState with complex states

TypeScript can infer simple useState types, but it fails with complex objects, null unions, and API data. Explicit typing prevents runtime errors and improves autocomplete.

When inference fails:

// ❌ TypeScript infers type as undefined, can't add user later
const [user, setUser] = useState();
// Later in code...
setUser({ id: 1, name: 'John' }); // Error: Argument of type '{ id: number; name: string; }' is not assignable

Null unions and API data patterns:

// ✅ Explicit typing with null union
type User = {
id: number;
name: string;
email: string;
avatar?: string;
};
const [user, setUser] = useState<User | null>(null);
// ✅ TypeScript knows user can be null
if (user) {
console.log(user.name); // Safe access
}

Auth/profile data example:

type AuthState = {
user: User | null;
isAuthenticated: boolean;
isLoading: boolean;
};
function useAuth() {
const [auth, setAuth] = useState<AuthState>({
user: null,
isAuthenticated: false,
isLoading: true,
});
const login = async (email: string, password: string) => {
setAuth((prev) => ({ ...prev, isLoading: true }));
try {
const user = await api.login(email, password);
setAuth({ user, isAuthenticated: true, isLoading: false });
} catch (error) {
setAuth({ user: null, isAuthenticated: false, isLoading: false });
}
};
return { auth, login };
}

Mistake 6: Incorrect useRef typing

Refs need careful typing because they start as null and get assigned later. Mistyping refs leads to runtime errors when accessing .current.

Why refs need null initial value:

// ❌ Wrong - TypeScript thinks ref is always HTMLInputElement
const inputRef = useRef<HTMLInputElement>();
// Later...
inputRef.current.focus(); // Error: Object is possibly 'undefined'

Matching element types:

// ✅ Correct - ref can be null initially
const inputRef = useRef<HTMLInputElement>(null);
// ✅ Safe access with optional chaining
const focusInput = () => {
inputRef.current?.focus();
};
return <input ref={inputRef} />;

Mutable value refs vs DOM refs:

// ✅ DOM ref - starts as null
const buttonRef = useRef<HTMLButtonElement>(null);
// ✅ Mutable value ref - doesn't need null
const renderCount = useRef<number>(0);
useEffect(() => {
renderCount.current += 1;
});
// ✅ Storing previous value
const prevValue = useRef<string>();
useEffect(() => {
prevValue.current = value;
}, [value]);

Mistake 7: useReducer without discriminated unions

String-based action types are error-prone. Discriminated unions make reducers type-safe and eliminate entire classes of bugs.

Why string action types are risky:

// ❌ Unsafe - typos compile but break at runtime
function reducer(state, action) {
switch (action.type) {
case 'LOAD_START':
return { ...state, loading: true };
case 'LOAD_SUCESS': // Typo! This case never matches
return { ...state, loading: false, data: action.payload };
}
}

Discriminated unions enforce strictness:

// ✅ Type-safe reducer with discriminated unions
type LoadingState = {
status: 'idle' | 'loading' | 'success' | 'error';
data: string | null;
error: string | null;
};
type Action =
| { type: 'LOAD_START' }
| { type: 'LOAD_SUCCESS'; payload: string }
| { type: 'LOAD_ERROR'; error: string };
function reducer(state: LoadingState, action: Action): LoadingState {
switch (action.type) {
case 'LOAD_START':
return { status: 'loading', data: null, error: null };
case 'LOAD_SUCCESS':
return { status: 'success', data: action.payload, error: null };
case 'LOAD_ERROR':
return { status: 'error', data: null, error: action.error };
default:
return state;
}
}

Example with loading, success, error:

function DataFetcher() {
const [state, dispatch] = useReducer(reducer, {
status: 'idle',
data: null,
error: null,
});
const fetchData = async () => {
dispatch({ type: 'LOAD_START' });
try {
const data = await api.getData();
dispatch({ type: 'LOAD_SUCCESS', payload: data });
} catch (error) {
dispatch({ type: 'LOAD_ERROR', error: error.message });
}
};
return (
<div>
{state.status === 'loading' && <Spinner />}
{state.status === 'success' && <div>{state.data}</div>}
{state.status === 'error' && <Error message={state.error} />}
</div>
);
}

Mistake 8: useContext without proper type guards

Context can resolve to undefined if consumed outside a provider. Type guards prevent runtime crashes and improve developer experience.

Why context can be undefined:

// ❌ Context might be undefined
const UserContext = createContext<User | undefined>(undefined);
function useUser() {
const user = useContext(UserContext);
return user; // Could be undefined!
}
// Later...
function Profile() {
const user = useUser();
return <div>{user.name}</div>; // Runtime error if no provider!
}

Safe custom hook pattern:

// ✅ Type guard ensures context is always defined
const UserContext = createContext<User | undefined>(undefined);
function useUser() {
const user = useContext(UserContext);
if (!user) {
throw new Error('useUser must be used within UserProvider');
}
return user;
}
// Now user is always defined
function Profile() {
const user = useUser(); // TypeScript knows user is User, not User | undefined
return <div>{user.name}</div>; // Safe!
}

Complete example:

type Theme = {
primary: string;
secondary: string;
background: string;
};
const ThemeContext = createContext<Theme | undefined>(undefined);
export function ThemeProvider({ children }: { children: React.ReactNode }) {
const theme: Theme = {
primary: '#007bff',
secondary: '#6c757d',
background: '#ffffff',
};
return (
<ThemeContext.Provider value={theme}>{children}</ThemeContext.Provider>
);
}
export function useTheme() {
const theme = useContext(ThemeContext);
if (!theme) {
throw new Error('useTheme must be used within ThemeProvider');
}
return theme;
}

Mistake 9: useMemo and useCallback type inference issues

TypeScript usually infers types for useMemo and useCallback, but complex scenarios require explicit typing for correctness and performance.

When explicit typing is necessary:

// ❌ Type inference fails with complex return types
const processedData = useMemo(() => {
return items.map((item) => ({
...item,
computed: expensiveCalculation(item),
}));
}, [items]);
// TypeScript might infer too broad a type

Generic parameters:

// ✅ Explicit typing for clarity
type ProcessedItem = {
id: number;
name: string;
computed: number;
};
const processedData = useMemo<ProcessedItem[]>(() => {
return items.map((item) => ({
id: item.id,
name: item.name,
computed: expensiveCalculation(item),
}));
}, [items]);

Dependency array type checking:

// ✅ useCallback with explicit types
const handleSubmit = useCallback(
(event: React.FormEvent<HTMLFormElement>) => {
event.preventDefault();
onSubmit(formData);
},
[formData, onSubmit], // TypeScript checks these dependencies
);

Props and Component pattern mistakes

Mistake 10: Not using utility types for props

Utility types like Pick, Omit, and Partial reduce duplication and make prop types more maintainable.

Common utility types:

type User = {
id: number;
name: string;
email: string;
password: string;
createdAt: Date;
};
// ✅ Pick - select specific properties
type UserPreview = Pick<User, 'id' | 'name'>;
// ✅ Omit - exclude properties
type PublicUser = Omit<User, 'password'>;
// ✅ Partial - make all properties optional
type UserUpdate = Partial<User>;
// ✅ Required - make all properties required
type CompleteUser = Required<User>;
// ✅ Readonly - make all properties readonly
type ImmutableUser = Readonly<User>;
// ✅ Record - create object type with specific keys
type UserRoles = Record<'admin' | 'user' | 'guest', boolean>;

Design system usage:

// ✅ Base button props
type BaseButtonProps = {
variant: 'primary' | 'secondary' | 'danger';
size: 'sm' | 'md' | 'lg';
disabled?: boolean;
loading?: boolean;
};
// ✅ Icon button omits size, adds icon
type IconButtonProps = Omit<BaseButtonProps, 'size'> & {
icon: React.ReactNode;
};
// ✅ Link button picks variant, adds href
type LinkButtonProps = Pick<BaseButtonProps, 'variant'> & {
href: string;
external?: boolean;
};

Mistake 11: Incorrect children prop typing

The children prop has multiple possible types. Using the wrong one causes type errors and limits component flexibility.

When to use each type:

// ✅ ReactNode - accepts anything renderable
type ContainerProps = {
children: React.ReactNode; // string, number, JSX, array, null, etc.
};
function Container({ children }: ContainerProps) {
return <div className="container">{children}</div>;
}
// ✅ ReactElement - only accepts JSX elements
type WrapperProps = {
children: React.ReactElement; // Must be a single JSX element
};
function Wrapper({ children }: WrapperProps) {
return <div className="wrapper">{children}</div>;
}
// ✅ JSX.Element - similar to ReactElement
type LayoutProps = {
children: JSX.Element;
};
// ✅ string - only accepts strings
type LabelProps = {
children: string;
};
function Label({ children }: LabelProps) {
return <label>{children.toUpperCase()}</label>;
}

Render prop patterns:

// ✅ Render prop with function type
type DataListProps<T> = {
data: T[];
renderItem: (item: T, index: number) => React.ReactNode;
};
function DataList<T>({ data, renderItem }: DataListProps<T>) {
return (
<ul>
{data.map((item, index) => (
<li key={index}>{renderItem(item, index)}</li>
))}
</ul>
);
}
// Usage
<DataList data={users} renderItem={(user) => <UserCard user={user} />} />;

Mistake 12: Not typing prop spreading

Extending native HTML attributes improves autocomplete and type safety when spreading props.

Extending HTML attributes:

// ❌ Props don't extend native attributes
type ButtonProps = {
variant: 'primary' | 'secondary';
};
function Button({ variant, ...props }: ButtonProps) {
return <button {...props} className={`btn btn--${variant}`} />;
}
// No autocomplete for onClick, disabled, etc.
// ✅ Extend native button attributes
type ButtonProps = React.ButtonHTMLAttributes<HTMLButtonElement> & {
variant: 'primary' | 'secondary';
};
function Button({ variant, ...props }: ButtonProps) {
return <button {...props} className={`btn btn--${variant}`} />;
}
// Full autocomplete for all button attributes!

ComponentPropsWithoutRef and ComponentPropsWithRef:

// ✅ ComponentPropsWithoutRef - for components without ref
type InputProps = React.ComponentPropsWithoutRef<'input'> & {
label: string;
error?: string;
};
function Input({ label, error, ...props }: InputProps) {
return (
<div>
<label>{label}</label>
<input {...props} />
{error && <span>{error}</span>}
</div>
);
}
// ✅ ComponentPropsWithRef - for components with ref
type ButtonProps = React.ComponentPropsWithRef<'button'> & {
variant: 'primary' | 'secondary';
};
const Button = forwardRef<HTMLButtonElement, ButtonProps>(
({ variant, ...props }, ref) => {
return <button ref={ref} {...props} className={`btn btn--${variant}`} />;
},
);

Mistake 13: Poor generic component typing

Generic components are powerful but easy to mistype. Proper constraints and polymorphic patterns make them type-safe.

Common pitfalls:

// ❌ No constraints - T could be anything
function List<T>({ items }: { items: T[] }) {
return (
<ul>
{items.map((item) => (
<li>{item}</li>
))}
</ul>
);
}
// Error: 'item' is of type 'unknown'

Proper constraint usage:

// ✅ Constrain T to have an id
type HasId = {
id: string | number;
};
function List<T extends HasId>({ items }: { items: T[] }) {
return (
<ul>
{items.map((item) => (
<li key={item.id}>{JSON.stringify(item)}</li>
))}
</ul>
);
}

Polymorphic component pattern (the "as" prop):

// ✅ Polymorphic component
type BoxProps<T extends React.ElementType = 'div'> = {
as?: T;
children: React.ReactNode;
}
function Box<T extends React.ElementType = 'div'>({
as,
children,
...props
}: BoxProps<T> & Omit<React.ComponentPropsWithoutRef<T>, keyof BoxProps<T>>) {
const Component = as || 'div';
return <Component {...props}>{children}</Component>;
}
// Usage
<Box>Default div</Box>
<Box as="section">Section element</Box>
<Box as="a" href="/home">Link element with href autocomplete!</Box>

Flexible Table component:

type Column<T> = {
key: keyof T;
header: string;
render?: (value: T[keyof T], item: T) => React.ReactNode;
};
type TableProps<T extends Record<string, any>> = {
data: T[];
columns: Column<T>[];
};
function Table<T extends Record<string, any>>({
data,
columns,
}: TableProps<T>) {
return (
<table>
<thead>
<tr>
{columns.map((col) => (
<th key={String(col.key)}>{col.header}</th>
))}
</tr>
</thead>
<tbody>
{data.map((item, idx) => (
<tr key={idx}>
{columns.map((col) => (
<td key={String(col.key)}>
{col.render
? col.render(item[col.key], item)
: String(item[col.key])}
</td>
))}
</tr>
))}
</tbody>
</table>
);
}

Mistake 14: Type narrowing and type guards

Union types are common in React (loading states, conditional rendering), but TypeScript needs help narrowing them.

Not properly narrowing union types:

// ❌ TypeScript can't narrow the type
type State =
| { status: 'loading' }
| { status: 'success'; data: string }
| { status: 'error'; error: string };
function Display({ state }: { state: State }) {
if (state.status === 'success') {
return <div>{state.data}</div>; // Error: Property 'data' does not exist on type 'State'
}
}

Creating custom type guard functions:

// ✅ Type guard function
function isSuccessState(
state: State,
): state is { status: 'success'; data: string } {
return state.status === 'success';
}
function Display({ state }: { state: State }) {
if (isSuccessState(state)) {
return <div>{state.data}</div>; // ✅ TypeScript knows state has data
}
}

Using discriminated unions effectively:

// ✅ Discriminated union with type narrowing
type ApiState<T> =
| { status: 'idle' }
| { status: 'loading' }
| { status: 'success'; data: T }
| { status: 'error'; error: string };
function DataDisplay<T>({ state }: { state: ApiState<T> }) {
switch (state.status) {
case 'idle':
return <div>Click to load</div>;
case 'loading':
return <Spinner />;
case 'success':
return <div>{JSON.stringify(state.data)}</div>; // ✅ data is available
case 'error':
return <Error message={state.error} />; // ✅ error is available
}
}

The "in" operator and typeof checks:

// ✅ Using "in" operator
type Dog = {
bark: () => void;
};
type Cat = {
meow: () => void;
};
type Pet = Dog | Cat;
function makeSound(pet: Pet) {
if ('bark' in pet) {
pet.bark(); // TypeScript knows it's a Dog
} else {
pet.meow(); // TypeScript knows it's a Cat
}
}
// ✅ Using typeof
function processValue(value: string | number) {
if (typeof value === 'string') {
return value.toUpperCase(); // TypeScript knows it's a string
} else {
return value.toFixed(2); // TypeScript knows it's a number
}
}

API and external integration mistakes

Mistake 15: Not typing API responses

Assuming API shape without types is dangerous. Create response types and consider runtime validation.

Risk of assuming API shape:

// ❌ No typing - runtime errors waiting to happen
async function fetchUser(id: number) {
const response = await fetch(`/api/users/${id}`);
const user = await response.json();
return user; // Type is 'any'
}

Generic response type:

// ✅ Type API responses
type ApiResponse<T> = {
success: boolean;
data?: T;
error?: string;
};
type User = {
id: number;
name: string;
email: string;
};
async function fetchUser(id: number): Promise<ApiResponse<User>> {
try {
const response = await fetch(`/api/users/${id}`);
const data = await response.json();
return { success: true, data };
} catch (error) {
return { success: false, error: error.message };
}
}

Runtime validation with Zod:

// ✅ Runtime validation with Zod
import { z } from 'zod';
const UserSchema = z.object({
id: z.number(),
name: z.string(),
email: z.string().email(),
});
type User = z.infer<typeof UserSchema>;
async function fetchUser(id: number): Promise<User> {
const response = await fetch(`/api/users/${id}`);
const data = await response.json();
return UserSchema.parse(data); // Throws if data doesn't match schema
}

Mistake 16: Missing return type annotations for async functions

Explicit Promise<T> return types improve clarity and help catch errors early.

Why explicit Promise<T> helps:

// ❌ Implicit return type - unclear what's returned
async function loadData() {
const response = await fetch('/api/data');
return response.json();
}
// ✅ Explicit return type - clear contract
async function loadData(): Promise<{ items: string[] }> {
const response = await fetch('/api/data');
return response.json();
}

Impact on debugging:

// ✅ TypeScript catches mismatches immediately
async function getUser(id: number): Promise<User> {
const response = await fetch(`/api/users/${id}`);
const data = await response.json();
return data; // Error if data doesn't match User type
}

Mistake 17: Working with poorly-typed third-party libraries

Not all libraries have good TypeScript support. Learn to augment types when needed.

Understanding @types/* packages:

# Install type definitions for libraries without built-in types
npm install --save-dev @types/lodash
npm install --save-dev @types/react-router-dom

Module augmentation:

// ✅ Augment existing module types
import 'react';
declare module 'react' {
interface CSSProperties {
'--custom-property'?: string;
}
}
// Now you can use custom CSS properties
<div style={{ '--custom-property': 'value' }} />;

Creating your own type declarations:

// ✅ Declare module for untyped library
declare module 'some-untyped-library' {
export function doSomething(value: string): number;
export interface Config {
apiKey: string;
timeout?: number;
}
}

The declare module pattern:

// types/custom.d.ts
declare module '*.svg' {
const content: React.FunctionComponent<React.SVGAttributes<SVGElement>>;
export default content;
}
declare module '*.png' {
const value: string;
export default value;
}

Best practices summary

Components checklist

  • ✅ Use explicit prop typing instead of React.FC
  • ✅ Never use any for props
  • ✅ Type event handlers with specific event types
  • ✅ Extend native HTML attributes with ComponentPropsWithoutRef

Hooks checklist

  • ✅ Explicitly type useState for complex states
  • ✅ Initialize useRef with null for DOM refs
  • ✅ Use discriminated unions in useReducer
  • ✅ Add type guards to useContext hooks
  • ✅ Type useMemo and useCallback when inference fails

Patterns checklist

  • ✅ Use utility types (Pick, Omit, Partial) to reduce duplication
  • ✅ Choose correct children type (ReactNode, ReactElement, string)
  • ✅ Type generic components with proper constraints
  • ✅ Create custom type guards for union types
  • ✅ Use discriminated unions for state machines

API & external checklist

  • ✅ Type all API responses
  • ✅ Add explicit return types to async functions
  • ✅ Consider runtime validation with Zod or Yup
  • ✅ Augment third-party library types when needed
  • ✅ Use @types/* packages for untyped libraries

TypeScript configuration for React projects

A proper tsconfig.json is essential for catching errors and enabling the best TypeScript features.

Minimal tsconfig.json:

{
"compilerOptions": {
"target": "ES2020",
"lib": ["ES2020", "DOM", "DOM.Iterable"],
"jsx": "react-jsx",
"module": "ESNext",
"moduleResolution": "bundler",
"strict": true,
"noUncheckedIndexedAccess": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"resolveJsonModule": true,
"isolatedModules": true,
"noEmit": true
},
"include": ["src"],
"exclude": ["node_modules"]
}

Why strict mode matters:

Enabling "strict": true activates all strict type-checking options:

  • strictNullChecks - prevents null/undefined errors
  • strictFunctionTypes - ensures function parameter safety
  • strictBindCallApply - types bind/call/apply correctly
  • noImplicitAny - requires explicit types
  • noImplicitThis - prevents this confusion

Key flags explained:

  • jsx: "react-jsx" - Uses the new JSX transform (React 17+), no need to import React
  • jsx: "react" - Classic JSX transform, requires import React
  • noUncheckedIndexedAccess - Makes array access return T | undefined, preventing index errors
  • moduleResolution: "bundler" - Modern resolution for Vite/webpack (use "node" for older setups)

Troubleshooting common TypeScript errors

Top 5 confusing error messages:

1. "Type 'X' is not assignable to type 'Y'"

What it means: You're trying to use a value where TypeScript expects a different type.

Quick fix:

// Error: Type 'string' is not assignable to type 'number'
const age: number = '25';
// Fix: Convert the type
const age: number = parseInt('25');

2. "Object is possibly 'undefined'"

What it means: You're accessing a property that might not exist.

Quick fix:

// Error
const name = user.profile.name;
// Fix: Use optional chaining
const name = user.profile?.name;
// Or: Type guard
if (user.profile) {
const name = user.profile.name;
}

3. "Property 'X' does not exist on type 'Y'"

What it means: TypeScript doesn't know about that property.

Quick fix:

// Error: Property 'customProp' does not exist
<div customProp="value" />;
// Fix: Extend the type
declare module 'react' {
interface HTMLAttributes<T> {
customProp?: string;
}
}

4. "Type instantiation is excessively deep and possibly infinite"

What it means: Your types are too complex or recursive.

Quick fix:

// Simplify complex prop spreading
// Instead of spreading everything, be explicit
interface ButtonProps
extends Pick<
React.ButtonHTMLAttributes<HTMLButtonElement>,
'onClick' | 'disabled' | 'type'
> {
variant: string;
}

5. "Cannot find name 'React'"

What it means: React isn't imported (only needed with old JSX transform).

Quick fix:

// If using "jsx": "react-jsx", you don't need this
// If using "jsx": "react", add:
import React from 'react';

When to use // @ts-expect-error vs // @ts-ignore:

// ✅ Use @ts-expect-error - fails if error is fixed
// @ts-expect-error: Third-party library has wrong types
const result = poorlyTypedLibrary.method();
// ❌ Avoid @ts-ignore - silently ignores errors forever
// @ts-ignore
const result = poorlyTypedLibrary.method();

Tools and Resources

VS Code extensions:

  • Pretty TypeScript errors - Formats complex type errors
  • Total TypeScript - Inline TypeScript tips and best practices

Useful TypeScript tools:

  • ts-reset - Improves built-in TypeScript types for better DX
  • type-fest - Collection of essential TypeScript utility types
  • ts-pattern - Pattern matching library with excellent type inference
  • zod - Runtime validation with TypeScript type inference

TypeScript playground:

Recommended Reading:

Practice your skills: Ready to test your knowledge? Head over to GreatFrontEnd's TypeScript Interview Questions to practice and prepare for your next interview.


Common interview questions you are now ready for

After mastering these mistakes, you're prepared for these React TypeScript interview questions:

Junior Level:

  1. What's the difference between interface and type in TypeScript?
  2. How do you type a React component's props?
  3. What's the correct way to type a useState hook?
  4. How do you type event handlers in React?
  5. What's the difference between ReactNode and ReactElement?

Mid Level:

  1. Explain discriminated unions and when to use them in React
  2. How do you properly type a useReducer hook? 8. What's the difference between ComponentPropsWithRef and ComponentPropsWithoutRef?
  3. How do you create a type-safe context with TypeScript? 1
  4. Explain how to type a generic component in React

Senior Level:

  1. How do you implement polymorphic components with the "as" prop pattern?
  2. What are the tradeoffs between runtime validation (Zod) and compile-time types?
  3. How would you type a complex form library with dynamic fields?
  4. Explain module augmentation and when you'd use it
  5. How do you handle type narrowing in complex conditional rendering scenarios?

Conclusion

TypeScript mistakes in React can be frustrating, but they're also learning opportunities. Each any type you replace, each event handler you properly type, and each hook you correctly structure makes your codebase a bit more robust.

The patterns covered here - explicit prop typing, discriminated unions, type guards, and proper generic constraints - are widely used in production codebases. They're not just theoretical best practices, but practical solutions to common problems.

If you're working on an existing codebase, consider starting small. Pick one component that uses any and give it proper types. Try adding discriminated unions to a reducer. Add a type guard to a context hook. Each improvement helps.

As you apply these patterns, you'll likely notice:

  • Fewer unexpected errors
  • Easier onboarding for team members
  • More confidence when refactoring
  • Better IDE support
  • Clearer component interfaces

Want more practice? Check out GreatFrontEnd's TypeScript interview questions for more TypeScript interview questions on library APIs, utility types, algorithms, and building strong, typed components to prepare for your next interview.