TypeScript Integration
Add static typing for improved code quality and DX
TypeScript Integration with React
Master React development with TypeScript using free flashcards and spaced repetition practice. This lesson covers type definitions for components, props and state typing, hooks with TypeScript, and advanced patternsβessential concepts for building type-safe React applications.
TypeScript has become the industry standard for React development, offering compile-time error detection, superior IDE support, and self-documenting code. By the end of this lesson, you'll understand how to leverage TypeScript's powerful type system to catch bugs before they reach production and build more maintainable React applications.
Welcome to TypeScript + React π»βοΈ
Combining TypeScript with React transforms your development experience from error-prone guesswork to confident, predictable coding. TypeScript adds static type checking to JavaScript, catching mistakes during development rather than at runtime. For React developers, this means:
- Autocomplete superpowers: Your IDE knows exactly what props a component accepts
- Refactoring confidence: Rename a prop and TypeScript finds every usage
- Self-documenting code: Type definitions serve as inline documentation
- Fewer runtime errors: Type mismatches are caught before deployment
π‘ Did you know? According to a 2020 study, TypeScript reduces bugs by approximately 15% compared to plain JavaScript, with the biggest gains in large codebases.
Core Concepts
Setting Up TypeScript with React
To start a new React project with TypeScript:
npx create-react-app my-app --template typescript
For existing projects, install TypeScript and type definitions:
npm install --save typescript @types/react @types/react-dom @types/node
Your project needs a tsconfig.json configuration file. React apps typically use:
{
"compilerOptions": {
"target": "es5",
"lib": ["dom", "dom.iterable", "esnext"],
"allowJs": true,
"skipLibCheck": true,
"esModuleInterop": true,
"allowSyntheticDefaultImports": true,
"strict": true,
"forceConsistentCasingInFileNames": true,
"module": "esnext",
"moduleResolution": "node",
"resolveJsonModule": true,
"isolatedModules": true,
"noEmit": true,
"jsx": "react-jsx"
},
"include": ["src"]
}
Key settings explained:
"strict": true- Enables all strict type-checking options"jsx": "react-jsx"- Uses the new JSX transform (React 17+)"noEmit": true- TypeScript only checks types, doesn't output files
Typing Function Components
The most common pattern is using the React.FC (Function Component) type or explicitly typing props:
Method 1: Using React.FC
import React from 'react';
interface ButtonProps {
label: string;
onClick: () => void;
disabled?: boolean;
}
const Button: React.FC<ButtonProps> = ({ label, onClick, disabled = false }) => {
return (
<button onClick={onClick} disabled={disabled}>
{label}
</button>
);
};
Method 2: Explicit props typing (preferred by many)
interface ButtonProps {
label: string;
onClick: () => void;
disabled?: boolean;
}
const Button = ({ label, onClick, disabled = false }: ButtonProps) => {
return (
<button onClick={onClick} disabled={disabled}>
{label}
</button>
);
};
π‘ Tip: Many developers prefer the second approach because React.FC has some quirks (it implicitly includes children and makes typing return values stricter).
Key TypeScript features for props:
?makes a property optional:disabled?: boolean|creates union types:size: 'small' | 'medium' | 'large'- Default values provide fallback:
disabled = false
Typing Props and State
Props with complex types:
interface User {
id: number;
name: string;
email: string;
}
interface UserCardProps {
user: User;
onEdit: (userId: number) => void;
badges?: string[];
children: React.ReactNode;
}
const UserCard = ({ user, onEdit, badges, children }: UserCardProps) => {
return (
<div>
<h2>{user.name}</h2>
<p>{user.email}</p>
{badges && <div>{badges.join(', ')}</div>}
<button onClick={() => onEdit(user.id)}>Edit</button>
{children}
</div>
);
};
Important prop types:
| Type | Usage | Example |
|---|---|---|
React.ReactNode | Any renderable content | children prop |
React.ReactElement | Specific React element | Single component child |
JSX.Element | JSX expression result | Component return type |
React.CSSProperties | Inline style object | style prop |
React.HTMLAttributes<T> | HTML element props | div, button attributes |
State typing with useState:
import { useState } from 'react';
// Simple state - type inference works
const [count, setCount] = useState(0); // inferred as number
// Complex state - explicit typing
interface FormState {
username: string;
email: string;
age: number | null;
}
const [formData, setFormData] = useState<FormState>({
username: '',
email: '',
age: null
});
// State that might be undefined initially
const [user, setUser] = useState<User | null>(null);
β οΈ Common mistake: Forgetting to handle null states!
// β BAD - user might be null!
const displayName = user.name;
// β
GOOD - handle null case
const displayName = user?.name ?? 'Guest';
Typing Hooks
useRef with DOM elements:
import { useRef, useEffect } from 'react';
const InputComponent = () => {
// Specify the element type
const inputRef = useRef<HTMLInputElement>(null);
useEffect(() => {
// TypeScript knows this might be null
inputRef.current?.focus();
}, []);
return <input ref={inputRef} />;
};
useRef for mutable values:
const timerRef = useRef<number>(0);
const startTimer = () => {
timerRef.current = window.setTimeout(() => {
console.log('Timer expired');
}, 1000);
};
useReducer with TypeScript:
import { useReducer } from 'react';
// Define state and action types
interface CounterState {
count: number;
lastAction: string;
}
type CounterAction =
| { type: 'increment' }
| { type: 'decrement' }
| { type: 'reset'; payload: number };
const counterReducer = (state: CounterState, action: CounterAction): CounterState => {
switch (action.type) {
case 'increment':
return { count: state.count + 1, lastAction: 'increment' };
case 'decrement':
return { count: state.count - 1, lastAction: 'decrement' };
case 'reset':
return { count: action.payload, lastAction: 'reset' };
default:
return state;
}
};
const Counter = () => {
const [state, dispatch] = useReducer(counterReducer, { count: 0, lastAction: 'none' });
return (
<div>
<p>Count: {state.count}</p>
<button onClick={() => dispatch({ type: 'increment' })}>+</button>
<button onClick={() => dispatch({ type: 'decrement' })}>-</button>
<button onClick={() => dispatch({ type: 'reset', payload: 0 })}>Reset</button>
</div>
);
};
π‘ Power pattern: Discriminated unions (like CounterAction above) give you exhaustive checking. TypeScript ensures you handle every action type in your switch statement!
useContext with TypeScript:
import { createContext, useContext, useState } from 'react';
interface ThemeContextType {
theme: 'light' | 'dark';
toggleTheme: () => void;
}
// Create context with undefined default (provide actual value in Provider)
const ThemeContext = createContext<ThemeContextType | undefined>(undefined);
// Custom hook with type safety
const useTheme = () => {
const context = useContext(ThemeContext);
if (context === undefined) {
throw new Error('useTheme must be used within ThemeProvider');
}
return context;
};
const ThemeProvider = ({ children }: { children: React.ReactNode }) => {
const [theme, setTheme] = useState<'light' | 'dark'>('light');
const toggleTheme = () => {
setTheme(prev => prev === 'light' ? 'dark' : 'light');
};
return (
<ThemeContext.Provider value={{ theme, toggleTheme }}>
{children}
</ThemeContext.Provider>
);
};
Event Handling
React events are SyntheticEvent types, not native browser events:
import React from 'react';
const FormComponent = () => {
// Generic form submission
const handleSubmit = (event: React.FormEvent<HTMLFormElement>) => {
event.preventDefault();
console.log('Form submitted');
};
// Input change events
const handleChange = (event: React.ChangeEvent<HTMLInputElement>) => {
console.log(event.target.value);
};
// Button clicks
const handleClick = (event: React.MouseEvent<HTMLButtonElement>) => {
console.log('Button clicked at:', event.clientX, event.clientY);
};
// Keyboard events
const handleKeyPress = (event: React.KeyboardEvent<HTMLInputElement>) => {
if (event.key === 'Enter') {
console.log('Enter pressed');
}
};
return (
<form onSubmit={handleSubmit}>
<input onChange={handleChange} onKeyPress={handleKeyPress} />
<button onClick={handleClick}>Submit</button>
</form>
);
};
Common event types:
| Event | Type | Target Element |
|---|---|---|
| Click | React.MouseEvent | <HTMLButtonElement> |
| Change | React.ChangeEvent | <HTMLInputElement> |
| Submit | React.FormEvent | <HTMLFormElement> |
| Keyboard | React.KeyboardEvent | <HTMLInputElement> |
| Focus | React.FocusEvent | <HTMLInputElement> |
Detailed Examples
Example 1: Typed Todo Application π
A complete todo app demonstrating props, state, and event typing:
import { useState } from 'react';
// Domain types
interface Todo {
id: number;
text: string;
completed: boolean;
createdAt: Date;
}
// Component props
interface TodoItemProps {
todo: Todo;
onToggle: (id: number) => void;
onDelete: (id: number) => void;
}
const TodoItem = ({ todo, onToggle, onDelete }: TodoItemProps) => {
return (
<div style={{ textDecoration: todo.completed ? 'line-through' : 'none' }}>
<input
type="checkbox"
checked={todo.completed}
onChange={() => onToggle(todo.id)}
/>
<span>{todo.text}</span>
<button onClick={() => onDelete(todo.id)}>Delete</button>
</div>
);
};
const TodoApp = () => {
const [todos, setTodos] = useState<Todo[]>([]);
const [inputValue, setInputValue] = useState('');
const addTodo = () => {
if (inputValue.trim() === '') return;
const newTodo: Todo = {
id: Date.now(),
text: inputValue,
completed: false,
createdAt: new Date()
};
setTodos(prev => [...prev, newTodo]);
setInputValue('');
};
const toggleTodo = (id: number) => {
setTodos(prev => prev.map(todo =>
todo.id === id ? { ...todo, completed: !todo.completed } : todo
));
};
const deleteTodo = (id: number) => {
setTodos(prev => prev.filter(todo => todo.id !== id));
};
return (
<div>
<h1>Todo List</h1>
<input
value={inputValue}
onChange={(e: React.ChangeEvent<HTMLInputElement>) => setInputValue(e.target.value)}
onKeyPress={(e: React.KeyboardEvent<HTMLInputElement>) => {
if (e.key === 'Enter') addTodo();
}}
/>
<button onClick={addTodo}>Add</button>
{todos.map(todo => (
<TodoItem
key={todo.id}
todo={todo}
onToggle={toggleTodo}
onDelete={deleteTodo}
/>
))}
</div>
);
};
Key TypeScript features used:
- Interface composition for complex domain models
- Function type signatures for callbacks:
(id: number) => void - Array typing with generics:
Todo[] - Event typing with specific elements
Example 2: Generic Component with Constraints π―
Generics make components reusable across different data types:
interface SelectOption {
value: string | number;
label: string;
}
interface SelectProps<T extends SelectOption> {
options: T[];
value: T['value'];
onChange: (value: T['value']) => void;
renderOption?: (option: T) => React.ReactNode;
}
function Select<T extends SelectOption>(
{ options, value, onChange, renderOption }: SelectProps<T>
) {
return (
<select
value={value}
onChange={(e: React.ChangeEvent<HTMLSelectElement>) => {
const selectedValue = e.target.value;
// Convert back to number if original was number
const option = options.find(opt => String(opt.value) === selectedValue);
if (option) onChange(option.value);
}}
>
{options.map(option => (
<option key={option.value} value={option.value}>
{renderOption ? renderOption(option) : option.label}
</option>
))}
</select>
);
}
// Usage with extended interface
interface ColorOption extends SelectOption {
hex: string;
}
const ColorPicker = () => {
const [color, setColor] = useState<string>('red');
const colors: ColorOption[] = [
{ value: 'red', label: 'Red', hex: '#ff0000' },
{ value: 'blue', label: 'Blue', hex: '#0000ff' },
{ value: 'green', label: 'Green', hex: '#00ff00' }
];
return (
<Select
options={colors}
value={color}
onChange={setColor}
renderOption={(option) => (
<span>
<span style={{ backgroundColor: option.hex, width: 10, height: 10, display: 'inline-block' }} />
{option.label}
</span>
)}
/>
);
};
Generic patterns explained:
<T extends SelectOption>- T must have at leastvalueandlabelT['value']- Index access type, gets the type of thevaluepropertyrenderOption?: (option: T) => React.ReactNode- Optional custom renderer
π‘ Pro tip: Generics with constraints give you type safety while maintaining flexibility!
Example 3: Custom Hook with TypeScript π£
Custom hooks benefit enormously from TypeScript:
import { useState, useEffect } from 'react';
// Hook return type
interface UseFetchResult<T> {
data: T | null;
loading: boolean;
error: Error | null;
refetch: () => void;
}
// Generic fetch hook
function useFetch<T>(url: string, options?: RequestInit): UseFetchResult<T> {
const [data, setData] = useState<T | null>(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<Error | null>(null);
const [refetchIndex, setRefetchIndex] = useState(0);
useEffect(() => {
const fetchData = async () => {
setLoading(true);
setError(null);
try {
const response = await fetch(url, options);
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const json = await response.json();
setData(json as T);
} catch (e) {
setError(e instanceof Error ? e : new Error('Unknown error'));
} finally {
setLoading(false);
}
};
fetchData();
}, [url, refetchIndex]);
const refetch = () => setRefetchIndex(prev => prev + 1);
return { data, loading, error, refetch };
}
// Usage with typed API response
interface User {
id: number;
name: string;
email: string;
}
const UserList = () => {
const { data: users, loading, error, refetch } = useFetch<User[]>(
'https://api.example.com/users'
);
if (loading) return <div>Loading...</div>;
if (error) return <div>Error: {error.message}</div>;
if (!users) return <div>No data</div>;
return (
<div>
{users.map(user => (
<div key={user.id}>
{user.name} - {user.email}
</div>
))}
<button onClick={refetch}>Refresh</button>
</div>
);
};
TypeScript advantages in hooks:
- Generic
<T>lets you specify return data type - Return type interface documents what the hook provides
- Null checks are enforced by the type system
- IDE autocomplete knows exact structure of
users
Example 4: Advanced Prop Patterns π
Discriminated unions for variant props:
// Different prop sets based on variant
type ButtonProps =
| {
variant: 'primary';
onClick: () => void;
label: string;
}
| {
variant: 'link';
href: string;
label: string;
external?: boolean;
}
| {
variant: 'icon';
icon: React.ReactNode;
onClick: () => void;
ariaLabel: string;
};
const Button = (props: ButtonProps) => {
// TypeScript knows which props are available based on variant
if (props.variant === 'primary') {
return <button onClick={props.onClick}>{props.label}</button>;
}
if (props.variant === 'link') {
return (
<a
href={props.href}
target={props.external ? '_blank' : undefined}
rel={props.external ? 'noopener noreferrer' : undefined}
>
{props.label}
</a>
);
}
// variant === 'icon'
return (
<button onClick={props.onClick} aria-label={props.ariaLabel}>
{props.icon}
</button>
);
};
// Usage - TypeScript enforces correct props for each variant
<Button variant="primary" onClick={() => {}} label="Click me" />
<Button variant="link" href="/home" label="Home" />
<Button variant="icon" icon={<span>π</span>} onClick={() => {}} ariaLabel="Search" />
Extending HTML element props:
import React from 'react';
// Inherit all button props + add custom ones
interface CustomButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
variant: 'primary' | 'secondary';
loading?: boolean;
}
const CustomButton = ({ variant, loading, children, ...rest }: CustomButtonProps) => {
return (
<button
{...rest}
className={`btn btn-${variant} ${rest.className || ''}`}
disabled={loading || rest.disabled}
>
{loading ? 'Loading...' : children}
</button>
);
};
// Now supports all standard button props + custom ones
<CustomButton
variant="primary"
loading={false}
onClick={() => console.log('clicked')}
type="submit"
disabled={false}
>
Submit
</CustomButton>
Common Mistakes β οΈ
1. Using any as an escape hatch
// β BAD - defeats the purpose of TypeScript
const handleData = (data: any) => {
console.log(data.name); // No type checking!
};
// β
GOOD - use unknown and type guards
const handleData = (data: unknown) => {
if (typeof data === 'object' && data !== null && 'name' in data) {
console.log((data as { name: string }).name);
}
};
// β
BETTER - define proper types
interface DataType {
name: string;
}
const handleData = (data: DataType) => {
console.log(data.name);
};
2. Forgetting to handle null/undefined
// β BAD - might crash if user is null
const UserProfile = ({ user }: { user: User | null }) => {
return <div>{user.name}</div>; // Error: user might be null
};
// β
GOOD - handle null case
const UserProfile = ({ user }: { user: User | null }) => {
if (!user) return <div>No user</div>;
return <div>{user.name}</div>;
};
// β
ALSO GOOD - optional chaining
const UserProfile = ({ user }: { user: User | null }) => {
return <div>{user?.name ?? 'Guest'}</div>;
};
3. Incorrect event types
// β BAD - using generic Event type
const handleClick = (e: Event) => {
console.log(e.target.value); // Error: Event doesn't have target.value
};
// β
GOOD - use specific React event type
const handleClick = (e: React.MouseEvent<HTMLButtonElement>) => {
console.log(e.currentTarget); // Typed as HTMLButtonElement
};
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
console.log(e.target.value); // Works!
};
4. Over-typing simple components
// β BAD - unnecessary complexity for simple cases
interface GreetingProps {
name: string;
}
const Greeting: React.FC<GreetingProps> = ({ name }): JSX.Element => {
return <div>Hello {name}</div>;
};
// β
GOOD - let TypeScript infer when possible
const Greeting = ({ name }: { name: string }) => {
return <div>Hello {name}</div>;
};
5. Not using discriminated unions
// β BAD - optional props create impossible states
interface ApiState {
loading: boolean;
data?: User[];
error?: string;
}
// Problem: Can have loading=false, data=undefined, error=undefined
// β
GOOD - discriminated union ensures valid states only
type ApiState =
| { status: 'idle' }
| { status: 'loading' }
| { status: 'success'; data: User[] }
| { status: 'error'; error: string };
// Now impossible to have invalid state combinations
6. Mutating props or state directly
// β BAD - mutating state directly
const addItem = () => {
items.push(newItem); // TypeScript won't stop this, but React will break
setItems(items);
};
// β
GOOD - create new array
const addItem = () => {
setItems([...items, newItem]);
};
// β
ALSO GOOD - use callback form
const addItem = () => {
setItems(prev => [...prev, newItem]);
};
Key Takeaways π―
β
Start strict: Enable "strict": true in tsconfig.json from day one
β
Interface for objects, type for unions: Use interface for object shapes, type for unions and complex types
β Let inference work: Don't over-annotate; TypeScript can infer many types automatically
β Handle null/undefined: Always account for nullable types with guards or optional chaining
β Use discriminated unions: Model state machines and variants with tagged unions
β Generic components: Use generics to make reusable components type-safe
β Proper event types: Use React's SyntheticEvent types, not native browser events
β Custom hooks return objects: Return objects with named properties, not arrays (unlike useState)
β
Extend HTML props: Use extends React.HTMLAttributes<T> to inherit native props
β
Gradual adoption: You can add TypeScript incrementally with .tsx files alongside .jsx
π Quick Reference Card
| Pattern | Syntax |
|---|---|
| Function Component | ({ prop }: Props) => JSX |
| Optional Prop | name?: string |
| Union Type | size: 'sm' | 'md' | 'lg' |
| State with Type | useState<User | null>(null) |
| Ref for DOM | useRef<HTMLInputElement>(null) |
| Event Handler | React.ChangeEvent<HTMLInputElement> |
| Children Prop | children: React.ReactNode |
| Generic Component | function Comp<T>(props: Props<T>) |
| Extend HTML Props | extends React.ButtonHTMLAttributes |
| Custom Hook Return | { data: T | null, loading: boolean } |
Memory aid - The TypeScript Trio:
- π Interface = Object shape definition
- π Type = Unions, aliases, complex types
- β‘ Generic = Flexible, reusable type variables
π Further Study
- React TypeScript Cheatsheet: https://react-typescript-cheatsheet.netlify.app/ - Comprehensive patterns and examples
- TypeScript Handbook - React: https://www.typescriptlang.org/docs/handbook/react.html - Official TypeScript documentation for React
- Total TypeScript - React Course: https://www.totaltypescript.com/tutorials/react-with-typescript - Free interactive tutorials
π§ Mnemonic for event types: "Change Forms, Click Buttons, Submit Forms, Type Keys" = ChangeEvent, MouseEvent, FormEvent, KeyboardEvent
π‘ Practice project idea: Convert an existing JavaScript React project to TypeScript file by file, starting with components that have the fewest dependencies.