
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.
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.
// ❌ Without TypeScript - Runtime error waiting to happenfunction UserProfile({ user }) {return <div>{user.profile.name}</div>;}// ✅ With TypeScript - Error caught at compile timetype 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.
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:
children in props (even when you don't want it)displayName, propTypes, and other legacy propertiesWhy explicit typing is clearer:
// ❌ Using React.FC - children included even when not neededconst 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 intentionaltype 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:
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 autocompletefunction 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 variantstype CardProps = {title: string;description?: string; // Optional propvariant?: 'primary' | 'secondary' | 'danger'; // Union type for variantsonClose?: () => 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.
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 safetyfunction SearchInput({ onChange }: { onChange: Function }) {return <input onChange={onChange} />;}// ❌ Using any - defeats the purposefunction SearchInput({ onChange }: { onChange: any }) {return <input onChange={onChange} />;}
Correct event type patterns:
// ✅ Proper event typingtype SearchInputProps = {onChange: (event: React.ChangeEvent<HTMLInputElement>) => void;};function SearchInput({ onChange }: SearchInputProps) {return <input type="text" onChange={onChange} />;}// Usage with full type safetyfunction 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 type | Event type | Common 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>);}
forwardRef typing confusionforwardRef 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 wrongconst 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 typingtype InputProps = {placeholder?: string;error?: boolean;};const Input = forwardRef<HTMLInputElement, InputProps>(({ placeholder, error }, ref) => {return (<inputref={ref}placeholder={placeholder}className={error ? 'input--error' : 'input'}/>);},);Input.displayName = 'Input';
Common error messages and fixes:
| Error message | Fix |
|---|---|
Type 'ForwardedRef<unknown>' is not assignable | Add generic types: forwardRef<ElementType, PropsType> |
Property 'displayName' does not exist | Add ComponentName.displayName = 'Name' after definition |
Type instantiation is excessively deep | Simplify prop spreading or use ComponentPropsWithoutRef |
useState with complex statesTypeScript 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 laterconst [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 uniontype User = {id: number;name: string;email: string;avatar?: string;};const [user, setUser] = useState<User | null>(null);// ✅ TypeScript knows user can be nullif (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 };}
useRef typingRefs 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 HTMLInputElementconst inputRef = useRef<HTMLInputElement>();// Later...inputRef.current.focus(); // Error: Object is possibly 'undefined'
Matching element types:
// ✅ Correct - ref can be null initiallyconst inputRef = useRef<HTMLInputElement>(null);// ✅ Safe access with optional chainingconst focusInput = () => {inputRef.current?.focus();};return <input ref={inputRef} />;
Mutable value refs vs DOM refs:
// ✅ DOM ref - starts as nullconst buttonRef = useRef<HTMLButtonElement>(null);// ✅ Mutable value ref - doesn't need nullconst renderCount = useRef<number>(0);useEffect(() => {renderCount.current += 1;});// ✅ Storing previous valueconst prevValue = useRef<string>();useEffect(() => {prevValue.current = value;}, [value]);
useReducer without discriminated unionsString-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 runtimefunction reducer(state, action) {switch (action.type) {case 'LOAD_START':return { ...state, loading: true };case 'LOAD_SUCESS': // Typo! This case never matchesreturn { ...state, loading: false, data: action.payload };}}
Discriminated unions enforce strictness:
// ✅ Type-safe reducer with discriminated unionstype 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>);}
useContext without proper type guardsContext 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 undefinedconst 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 definedconst 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 definedfunction Profile() {const user = useUser(); // TypeScript knows user is User, not User | undefinedreturn <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;}
useMemo and useCallback type inference issuesTypeScript 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 typesconst processedData = useMemo(() => {return items.map((item) => ({...item,computed: expensiveCalculation(item),}));}, [items]);// TypeScript might infer too broad a type
Generic parameters:
// ✅ Explicit typing for claritytype 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 typesconst handleSubmit = useCallback((event: React.FormEvent<HTMLFormElement>) => {event.preventDefault();onSubmit(formData);},[formData, onSubmit], // TypeScript checks these dependencies);
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 propertiestype UserPreview = Pick<User, 'id' | 'name'>;// ✅ Omit - exclude propertiestype PublicUser = Omit<User, 'password'>;// ✅ Partial - make all properties optionaltype UserUpdate = Partial<User>;// ✅ Required - make all properties requiredtype CompleteUser = Required<User>;// ✅ Readonly - make all properties readonlytype ImmutableUser = Readonly<User>;// ✅ Record - create object type with specific keystype UserRoles = Record<'admin' | 'user' | 'guest', boolean>;
Design system usage:
// ✅ Base button propstype BaseButtonProps = {variant: 'primary' | 'secondary' | 'danger';size: 'sm' | 'md' | 'lg';disabled?: boolean;loading?: boolean;};// ✅ Icon button omits size, adds icontype IconButtonProps = Omit<BaseButtonProps, 'size'> & {icon: React.ReactNode;};// ✅ Link button picks variant, adds hreftype LinkButtonProps = Pick<BaseButtonProps, 'variant'> & {href: string;external?: boolean;};
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 renderabletype ContainerProps = {children: React.ReactNode; // string, number, JSX, array, null, etc.};function Container({ children }: ContainerProps) {return <div className="container">{children}</div>;}// ✅ ReactElement - only accepts JSX elementstype WrapperProps = {children: React.ReactElement; // Must be a single JSX element};function Wrapper({ children }: WrapperProps) {return <div className="wrapper">{children}</div>;}// ✅ JSX.Element - similar to ReactElementtype LayoutProps = {children: JSX.Element;};// ✅ string - only accepts stringstype LabelProps = {children: string;};function Label({ children }: LabelProps) {return <label>{children.toUpperCase()}</label>;}
Render prop patterns:
// ✅ Render prop with function typetype 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} />} />;
Extending native HTML attributes improves autocomplete and type safety when spreading props.
Extending HTML attributes:
// ❌ Props don't extend native attributestype 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 attributestype 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 reftype 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 reftype ButtonProps = React.ComponentPropsWithRef<'button'> & {variant: 'primary' | 'secondary';};const Button = forwardRef<HTMLButtonElement, ButtonProps>(({ variant, ...props }, ref) => {return <button ref={ref} {...props} className={`btn btn--${variant}`} />;},);
Generic components are powerful but easy to mistype. Proper constraints and polymorphic patterns make them type-safe.
Common pitfalls:
// ❌ No constraints - T could be anythingfunction 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 idtype 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 componenttype 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>);}
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 typetype 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 functionfunction 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 narrowingtype 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 availablecase 'error':return <Error message={state.error} />; // ✅ error is available}}
The "in" operator and typeof checks:
// ✅ Using "in" operatortype 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 typeoffunction 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}}
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 happenasync 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 responsestype 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 Zodimport { 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}
Explicit Promise<T> return types improve clarity and help catch errors early.
Why explicit Promise<T> helps:
// ❌ Implicit return type - unclear what's returnedasync function loadData() {const response = await fetch('/api/data');return response.json();}// ✅ Explicit return type - clear contractasync function loadData(): Promise<{ items: string[] }> {const response = await fetch('/api/data');return response.json();}
Impact on debugging:
// ✅ TypeScript catches mismatches immediatelyasync 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}
Not all libraries have good TypeScript support. Learn to augment types when needed.
Understanding @types/* packages:
# Install type definitions for libraries without built-in typesnpm install --save-dev @types/lodashnpm install --save-dev @types/react-router-dom
Module augmentation:
// ✅ Augment existing module typesimport '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 librarydeclare module 'some-untyped-library' {export function doSomething(value: string): number;export interface Config {apiKey: string;timeout?: number;}}
The declare module pattern:
// types/custom.d.tsdeclare module '*.svg' {const content: React.FunctionComponent<React.SVGAttributes<SVGElement>>;export default content;}declare module '*.png' {const value: string;export default value;}
React.FCany for propsComponentPropsWithoutRefuseState for complex statesuseRef with null for DOM refsuseReduceruseContext hooksuseMemo and useCallback when inference failsPick, Omit, Partial) to reduce duplicationReactNode, ReactElement, string)@types/* packages for untyped librariesA 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 errorsstrictFunctionTypes - ensures function parameter safetystrictBindCallApply - types bind/call/apply correctlynoImplicitAny - requires explicit typesnoImplicitThis - prevents this confusionKey flags explained:
jsx: "react-jsx" - Uses the new JSX transform (React 17+), no need to import Reactjsx: "react" - Classic JSX transform, requires import ReactnoUncheckedIndexedAccess - Makes array access return T | undefined, preventing index errorsmoduleResolution: "bundler" - Modern resolution for Vite/webpack (use "node" for older setups)Top 5 confusing error messages:
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 typeconst age: number = parseInt('25');
What it means: You're accessing a property that might not exist.
Quick fix:
// Errorconst name = user.profile.name;// Fix: Use optional chainingconst name = user.profile?.name;// Or: Type guardif (user.profile) {const name = user.profile.name;}
What it means: TypeScript doesn't know about that property.
Quick fix:
// Error: Property 'customProp' does not exist<div customProp="value" />;// Fix: Extend the typedeclare module 'react' {interface HTMLAttributes<T> {customProp?: string;}}
What it means: Your types are too complex or recursive.
Quick fix:
// Simplify complex prop spreading// Instead of spreading everything, be explicitinterface ButtonPropsextends Pick<React.ButtonHTMLAttributes<HTMLButtonElement>,'onClick' | 'disabled' | 'type'> {variant: string;}
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 typesconst result = poorlyTypedLibrary.method();// ❌ Avoid @ts-ignore - silently ignores errors forever// @ts-ignoreconst result = poorlyTypedLibrary.method();
VS Code extensions:
Useful TypeScript tools:
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.
After mastering these mistakes, you're prepared for these React TypeScript interview questions:
Junior Level:
interface and type in TypeScript?useState hook?ReactNode and ReactElement?Mid Level:
useReducer hook? 8. What's the difference between ComponentPropsWithRef and ComponentPropsWithoutRef?Senior Level:
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:
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.