TypeScript adds static typing on top of JavaScript. In a React project, that means your components, props, state, refs, events, and API responses can all be described with types. The TypeScript compiler then checks your code before it runs, which helps catch mistakes such as missing props, wrong property names, incorrect function arguments, invalid event handling, or unsafe assumptions about fetched data.
Using React with TypeScript does not change the core React model. You still build components, pass props, manage state, and use hooks in the same way. The difference is that TypeScript makes those contracts explicit. This leads to safer refactoring, better autocomplete, easier onboarding, and fewer runtime surprises.
| Area | Benefit | Example |
|---|---|---|
| Props | Documents what a component expects | Required and optional props |
| State | Keeps state shape predictable | useState<User | null>() |
| Events | Gives correct event target types | React.ChangeEvent<HTMLInputElement> |
| Refs | Makes DOM access safer | useRef<HTMLInputElement>(null) |
| Hooks | Clarifies arguments and returns | useFetch<Product[]>() |
| API responses | Prevents unsafe data assumptions | ApiResponse<User[]> |
import { ReactNode } from 'react'
type ButtonVariant = 'primary' | 'secondary' | 'danger'
interface ButtonProps {
label: string
variant?: ButtonVariant
disabled?: boolean
onClick?: () => void
children?: ReactNode
}
function Button({ label, variant = 'primary', disabled = false, onClick, children }: ButtonProps) {
return (
<button className={`btn btn-${variant}`} disabled={disabled} onClick={onClick}>
{children ?? label}
</button>
)
}
export default Button
interface User {
id: number
name: string
email: string
role: 'admin' | 'editor' | 'user'
}
interface ProfileCardProps {
user: User
showEmail?: boolean
onPromote?: (userId: number) => void
}
function ProfileCard({ user, showEmail = true, onPromote }: ProfileCardProps) {
return (
<article>
<h3>{user.name}</h3>
{showEmail && <p>{user.email}</p>}
<p>Role: {user.role}</p>
{onPromote && user.role !== 'admin' && (
<button onClick={() => onPromote(user.id)}>Promote User</button>
)}
</article>
)
}
export default ProfileCard
import Button from './Button'
import ProfileCard from './ProfileCard'
function App() {
const user = {
id: 1,
name: 'Anita',
email: 'anita@example.com',
role: 'editor' as const,
}
return (
<div>
<Button label="Save" onClick={() => console.log('Saved')} />
<ProfileCard user={user} onPromote={(id) => console.log('Promote', id)} />
</div>
)
}
export default App
import { ChangeEvent, FormEvent, useState } from 'react'
interface LoginValues {
email: string
password: string
rememberMe: boolean
}
function LoginForm() {
const [values, setValues] = useState<LoginValues>({
email: '',
password: '',
rememberMe: false,
})
const [error, setError] = useState<string | null>(null)
function handleChange(event: ChangeEvent<HTMLInputElement>) {
const { name, value, type, checked } = event.target
setValues(current => ({
...current,
[name]: type === 'checkbox' ? checked : value,
}))
}
function handleSubmit(event: FormEvent<HTMLFormElement>) {
event.preventDefault()
if (!values.email || !values.password) {
setError('Email and password are required.')
return
}
setError(null)
console.log(values)
}
return (
<form onSubmit={handleSubmit}>
<input name="email" value={values.email} onChange={handleChange} placeholder="Email" />
<input name="password" type="password" value={values.password} onChange={handleChange} placeholder="Password" />
<label>
<input name="rememberMe" type="checkbox" checked={values.rememberMe} onChange={handleChange} />
Remember me
</label>
{error && <p>{error}</p>}
<button type="submit">Login</button>
</form>
)
}
export default LoginForm
import { useEffect, useRef } from 'react'
function FocusInput() {
const inputRef = useRef<HTMLInputElement>(null)
useEffect(() => {
inputRef.current?.focus()
}, [])
return <input ref={inputRef} placeholder="Focused on mount" />
}
export default FocusInput
export interface User {
id: number
name: string
email: string
role: 'admin' | 'user'
}
export interface ApiResponse<T> {
data: T
message: string
success: boolean
}
import { useEffect, useState } from 'react'
function useFetch<T>(url: string) {
const [data, setData] = useState<T | null>(null)
const [loading, setLoading] = useState(true)
const [error, setError] = useState<string | null>(null)
useEffect(() => {
let ignore = false
async function load() {
try {
setLoading(true)
const response = await fetch(url)
const result = await response.json() as T
if (!ignore) setData(result)
} catch (err) {
if (!ignore) setError(err instanceof Error ? err.message : 'Unknown error')
} finally {
if (!ignore) setLoading(false)
}
}
load()
return () => { ignore = true }
}, [url])
return { data, loading, error }
}
export default useFetch
import useFetch from './useFetch'
import { ApiResponse, User } from './types'
function UsersPage() {
const { data, loading, error } = useFetch<ApiResponse<User[]>>('/api/users')
if (loading) return <p>Loading users...</p>
if (error) return <p>Error: {error}</p>
return (
<ul>
{data?.data.map(user => (
<li key={user.id}>{user.name} ({user.role})</li>
))}
</ul>
)
}
export default UsersPage
| Mistake | Why it hurts | Better approach |
|---|---|---|
Using any too much | Removes TypeScript safety | Prefer interfaces, unions, and generics |
Ignoring null | Leads to unsafe access | Use union types like User | null |
| Typing everything manually | Creates noisy code | Let inference handle simple cases |
| Using broad string types | Allows invalid values | Use unions for variants and statuses |
User, Product, and Orderany unless you truly have an unknown boundaryReact with TypeScript gives you the same component-driven development style, but with stronger guarantees around how data flows through the application. Types make component contracts clearer, reduce runtime surprises, and make large codebases easier to refactor safely.
The most useful mindset is to treat types as contracts. A typed component clearly says what it accepts, a typed hook clearly says what it returns, and a typed API model clearly says what the application expects from the server.
Explore 500+ free tutorials across 20+ languages and frameworks.