1. Introduction
TypeScript adds a static type system on top of JavaScript, catching a whole category of bugs â wrong prop types, undefined values, mismatched function signatures â before your code ever runs. Combined with React, it turns components into self-documenting, autocomplete-friendly building blocks.
This tutorial walks through everything you need to type React applications confidently: components, hooks, events, context, forms, and more.
Information
2. Why Use TypeScript with React? đĄ
- Catch bugs early â typos in prop names or wrong data shapes are caught at compile time, not in production.
- Better editor support â autocomplete, inline docs, and "go to definition" all improve dramatically.
- Safer refactoring â renaming a prop or changing a type immediately flags every place that needs updating.
- Self-documenting code â types describe the shape of props, state, and data without needing separate docs.
3. Setting Up React with TypeScript âī¸
The fastest way to start a new project is via Vite, which ships first-class TypeScript templates.
Creating a new project
npm create vite@latest my-app -- --template react-ts
cd my-app
npm install
npm run devFor an existing JavaScript project, add TypeScript incrementally by installing the necessary packages and a tsconfig.json.
Adding TypeScript to an existing project
npm install -D typescript @types/react @types/react-dom4. Project Structure đī¸
TypeScript React files use the .tsx extension for files containing JSX, and plain .ts for files that don't render markup.
Tip
5. Typing Components đ§Š
A function component in TypeScript is just a regular function that returns JSX â the return type is usually inferred, but props need explicit typing.
Greeting.tsx
interface GreetingProps {
name: string;
}
function Greeting({ name }: GreetingProps) {
return <p>Hello, {name}!</p>;
}Note
6. Typing Props đ
Props are typically defined with an interface (extendable, good for object shapes) or a type alias (more flexible, supports unions).
Prop patterns
interface ButtonProps {
label: string;
onClick: () => void;
disabled?: boolean; // optional prop
variant?: 'primary' | 'secondary'; // union of string literals
}
function Button({ label, onClick, disabled = false, variant = 'primary' }: ButtonProps) {
return (
<button className={variant} onClick={onClick} disabled={disabled}>
{label}
</button>
);
}7. Typing State đī¸
useState infers the type from its initial value in simple cases, but you'll often need an explicit generic for more complex or nullable state.
State typing examples
const [count, setCount] = useState(0); // inferred as number
interface User {
id: string;
name: string;
}
const [user, setUser] = useState<User | null>(null); // explicit union type8. Typing Events đąī¸
React provides specific synthetic event types for each element and event kind, ensuring the event object's properties (like target.value) are correctly typed.
Common event types
function handleChange(e: React.ChangeEvent<HTMLInputElement>) {
console.log(e.target.value); // typed as string
}
function handleClick(e: React.MouseEvent<HTMLButtonElement>) {
console.log(e.currentTarget);
}
function handleSubmit(e: React.FormEvent<HTMLFormElement>) {
e.preventDefault();
}Hint
9. Typing Functions đ§
Function props â like callbacks passed down to children â should be typed with explicit parameter and return types to avoid any creeping in.
Typed callback props
interface SearchBarProps {
onSearch: (query: string) => void;
onClear?: () => void;
}
function SearchBar({ onSearch, onClear }: SearchBarProps) {
return (
<input
onChange={(e) => onSearch(e.target.value)}
onBlur={() => onClear?.()}
/>
);
}10. Typing Children đļ
The children prop uses React's built-in React.ReactNode type, which covers strings, numbers, elements, fragments, and arrays thereof.
Card.tsx
interface CardProps {
title: string;
children: React.ReactNode;
}
function Card({ title, children }: CardProps) {
return (
<div className="card">
<h2>{title}</h2>
{children}
</div>
);
}Caution
11. Typing Refs đ
useRef needs a type argument matching the DOM element (or value) it will point to. Refs to DOM nodes should be initialized with null.
Typed refs
function TextInput() {
const inputRef = useRef<HTMLInputElement>(null);
const focus = () => {
inputRef.current?.focus(); // optional chaining, since current may be null
};
return <input ref={inputRef} onFocus={focus} />;
}12. Typing Hooks đĒ
Most built-in hooks are generic functions â you pass a type parameter in angle brackets (<T>) to tell TypeScript what shape of data they hold, as covered in detail in the next several sections.
13. Typing useState đĸ
useState patterns
// Inferred
const [isOpen, setIsOpen] = useState(false);
// Explicit union for nullable async data
const [error, setError] = useState<string | null>(null);
// Explicit array type
const [items, setItems] = useState<string[]>([]);Tip
14. Typing useEffect âŗ
useEffect itself needs no type parameters, but its cleanup function must return either a function or undefined â never a Promise directly.
Typed effect cleanup
useEffect(() => {
const id = setInterval(() => console.log('tick'), 1000);
return () => clearInterval(id); // cleanup, correctly typed as () => void
}, []);Warning
15. Typing useRef đ¯
useRef has two common typing patterns depending on purpose: DOM refs (read-only, initialized null) and mutable value refs (read-write, holding arbitrary data across renders).
Two useRef patterns
// DOM ref
const divRef = useRef<HTMLDivElement>(null);
// Mutable value ref (e.g. storing a timer ID)
const timerRef = useRef<number | undefined>(undefined);16. Typing useContext đ§
Typed context
interface Theme {
mode: 'light' | 'dark';
toggle: () => void;
}
const ThemeContext = createContext<Theme | undefined>(undefined);
function useTheme() {
const ctx = useContext(ThemeContext);
if (!ctx) throw new Error('useTheme must be used within a ThemeProvider');
return ctx; // narrowed to Theme, no longer undefined
}Best Practice
17. Typing useReducer đ
Typed reducer
interface State {
count: number;
}
type Action =
| { type: 'increment' }
| { type: 'decrement' }
| { type: 'set'; payload: number };
function reducer(state: State, action: Action): State {
switch (action.type) {
case 'increment':
return { count: state.count + 1 };
case 'decrement':
return { count: state.count - 1 };
case 'set':
return { count: action.payload };
}
}
const [state, dispatch] = useReducer(reducer, { count: 0 });Tip
18. Typing Custom Hooks đŖ
Custom hooks should have explicit return types â especially when returning a tuple (array), since TypeScript would otherwise infer a generic array type instead of a fixed-length tuple.
useToggle.ts
function useToggle(initial = false): [boolean, () => void] {
const [value, setValue] = useState(initial);
const toggle = () => setValue((v) => !v);
return [value, toggle];
}Caution
19. Typing Context API đ§ĩ
For larger apps, combine a typed context with a typed provider component to encapsulate both the value shape and the logic that produces it.
AuthContext.tsx
interface AuthContextValue {
user: User | null;
login: (email: string, password: string) => Promise<void>;
logout: () => void;
}
const AuthContext = createContext<AuthContextValue | undefined>(undefined);
function AuthProvider({ children }: { children: React.ReactNode }) {
const [user, setUser] = useState<User | null>(null);
const login = async (email: string, password: string) => {
// authentication logic
};
const logout = () => setUser(null);
return (
<AuthContext.Provider value={{ user, login, logout }}>
{children}
</AuthContext.Provider>
);
}20. Typing Forms đ
Form state is typically modeled as a single typed object, updated immutably on each field change.
Typed form state
interface SignupForm {
email: string;
password: string;
agreeToTerms: boolean;
}
function useSignupForm() {
const [form, setForm] = useState<SignupForm>({
email: '',
password: '',
agreeToTerms: false,
});
function updateField<K extends keyof SignupForm>(key: K, value: SignupForm[K]) {
setForm((prev) => ({ ...prev, [key]: value }));
}
return { form, updateField };
}Tip
21. Typing API Responses đ
Define interfaces matching your backend's response shape, and apply them to fetch or your HTTP client's generic return type.
Typed fetch
interface Post {
id: number;
title: string;
body: string;
}
async function fetchPosts(): Promise<Post[]> {
const res = await fetch('/api/posts');
if (!res.ok) throw new Error('Failed to fetch posts');
const data: Post[] = await res.json();
return data;
}Warning
22. Generics in React đ§Ŧ
Generic components let a single component work correctly across many data types while preserving full type safety â extremely useful for reusable list, table, or select components.
Generic List component
interface ListProps<T> {
items: T[];
renderItem: (item: T) => React.ReactNode;
}
function List<T>({ items, renderItem }: ListProps<T>) {
return <ul>{items.map((item, i) => <li key={i}>{renderItem(item)}</li>)}</ul>;
}
// Usage: T is inferred as `User`
<List items={users} renderItem={(user) => <span>{user.name}</span>} />;23. Utility Types đ ī¸
TypeScript's built-in utility types transform existing types without redefining them from scratch â extremely useful when working with prop types derived from data models.
| Utility Type | Effect |
|---|---|
| Partial<T> | Makes all properties of T optional |
| Pick<T, K> | Selects a subset of keys K from T |
| Omit<T, K> | Excludes keys K from T |
| Readonly<T> | Makes all properties immutable |
| Record<K, V> | Builds an object type with keys K mapped to values V |
Utility type example
interface User {
id: string;
name: string;
email: string;
}
// Only id and name are required for a preview card
type UserPreview = Pick<User, 'id' | 'name'>;
// All fields optional, for a partial update form
type UserUpdate = Partial<User>;24. Component Patterns with TypeScript đī¸
Use a discriminated union when a component's props change shape based on a variant or kind field, ensuring invalid prop combinations are rejected at compile time.
Discriminated union props
type AlertProps =
| { variant: 'success'; message: string }
| { variant: 'error'; message: string; retry: () => void };
function Alert(props: AlertProps) {
if (props.variant === 'error') {
return <div onClick={props.retry}>{props.message}</div>; // retry is safely typed here
}
return <div>{props.message}</div>;
}Polymorphic components accept an as prop to render as a different underlying element while keeping that element's own props type-checked.
Simplified polymorphic prop typing
type TextProps<T extends React.ElementType> = {
as?: T;
children: React.ReactNode;
} & React.ComponentPropsWithoutRef<T>;
function Text<T extends React.ElementType = 'span'>({ as, children, ...rest }: TextProps<T>) {
const Component = as || 'span';
return <Component {...rest}>{children}</Component>;
}25. Type Safety Best Practices â
- Enable strict mode in tsconfig.json â it catches far more real bugs than the default settings.
- Avoid any; prefer unknown when a type is genuinely not known yet, then narrow it before use.
- Let TypeScript infer types where possible â only annotate when inference is ambiguous (like useState with a nullable initial value).
- Validate external data (API responses, form input) at runtime with a schema library, since types disappear at compile time.
26. Common Type Errors â
| Error | Typical Cause |
|---|---|
| Object is possibly 'null' | Accessing .current on a ref without optional chaining or a null check |
| Type 'X' is not assignable to type 'Y' | Passing a prop of the wrong shape or missing a required field |
| Property does not exist on type 'EventTarget' | Using the wrong (or no) generic on a React event type |
| Cannot find module or its type declarations | A third-party library lacks @types/bundled types |
27. Performance Considerations âĄ
TypeScript itself has zero runtime cost â all type annotations are erased during compilation. However, type choices can still affect developer velocity and build times.
- Overly complex generic types can slow down the TypeScript compiler on large codebases.
- Prefer interface over deeply nested type intersections for large object shapes â interfaces tend to check faster.
- Use type-only imports (import type { User } from './types') so bundlers can safely strip them, keeping bundle size unaffected by type usage.
28. Frequently Asked Questions â
Question
Answer
Question
Answer
Question
Answer
29. Summary đ
Summary
The result is a codebase that's easier to refactor, easier to onboard into, and far less prone to an entire class of runtime bugs. Happy typing! đ