Follow Us

CodeWithSabir

  • Contact Us
  • Privacy Policy
  • About
  • Terms & Conditions

All Rights Reserved © 2026

  • Light
  • Dark
Web Development

TypeScript Best Practices for React Developers

Sabir Soft
Sabir Lkhaloufi
  • December 15, 2025
  • 4 min read

TypeScript Best Practices for React Developers

TypeScript adds a compiler — it doesn't automatically make your React code correct. A codebase littered with any, unchecked as casts, and ! non-null assertions has types in name only. Real TypeScript proficiency means using the type system to catch real bugs, not just satisfying the compiler.

These are the patterns that actually matter in production React applications.

Type Your Props Precisely

The most common mistake: using any or overly broad types for props.

// BAD — type system does nothing for you
interface ButtonProps {
  onClick: any
  variant: string
  children: any
}
 
// GOOD — precise types catch mistakes at compile time
type ButtonVariant = 'primary' | 'secondary' | 'danger' | 'ghost'
 
interface ButtonProps {
  onClick: (event: React.MouseEvent<HTMLButtonElement>) => void
  variant: ButtonVariant
  children: React.ReactNode
  disabled?: boolean
  loading?: boolean
  type?: 'button' | 'submit' | 'reset'
}

With the precise version, passing variant="primry" (typo) is a compile error. With string, it silently fails at runtime.

Discriminated Unions for Component States

One of the most powerful patterns for modeling complex component states:

// Instead of multiple optional fields that can be in impossible combinations:
interface DataState {
  loading: boolean
  data?: User[]
  error?: string
}
// Problem: what does { loading: false, data: undefined, error: undefined } mean?
 
// Use a discriminated union — each state is unambiguous:
type AsyncState<T> =
  | { status: 'idle' }
  | { status: 'loading' }
  | { status: 'success'; data: T }
  | { status: 'error'; error: string }
 
function UserList() {
  const [state, setState] = useState<AsyncState<User[]>>({ status: 'idle' })
 
  // TypeScript narrows the type in each branch:
  if (state.status === 'loading') return <Spinner />
  if (state.status === 'error') return <ErrorMessage message={state.error} />
  if (state.status === 'idle') return <button onClick={fetchUsers}>Load Users</button>
 
  // state.status === 'success' is the only remaining case
  // TypeScript knows state.data is User[] here — no undefined check needed
  return <ul>{state.data.map(user => <li key={user.id}>{user.name}</li>)}</ul>
}

Generic Components

When you find yourself writing nearly identical components for different data types, use generics:

// Without generics — duplicate code for every type
interface UserSelectProps {
  options: User[]
  value: User | null
  onChange: (value: User | null) => void
}
 
interface PostSelectProps {
  options: Post[]
  value: Post | null
  onChange: (value: Post | null) => void
}
 
// With generics — one component, fully type-safe for any type
interface SelectProps<T> {
  options: T[]
  value: T | null
  onChange: (value: T | null) => void
  getLabel: (item: T) => string
  getKey: (item: T) => string
}
 
function Select<T>({ options, value, onChange, getLabel, getKey }: SelectProps<T>) {
  return (
    <select
      value={value ? getKey(value) : ''}
      onChange={e => {
        const selected = options.find(o => getKey(o) === e.target.value) ?? null
        onChange(selected)
      }}
    >
      <option value="">Select...</option>
      {options.map(option => (
        <option key={getKey(option)} value={getKey(option)}>
          {getLabel(option)}
        </option>
      ))}
    </select>
  )
}
 
// Usage — TypeScript infers T = User automatically
<Select
  options={users}
  value={selectedUser}
  onChange={setSelectedUser}
  getLabel={user => user.name}
  getKey={user => user.id}
/>

Typing Custom Hooks

Custom hooks deserve precise return types:

// Return tuple type — matches useState pattern
function useToggle(initial = false): [boolean, () => void, () => void, () => void] {
  const [value, setValue] = useState(initial)
 
  const on = useCallback(() => setValue(true), [])
  const off = useCallback(() => setValue(false), [])
  const toggle = useCallback(() => setValue(v => !v), [])
 
  return [value, toggle, on, off]
}
 
// Return object type — better for hooks with many values
interface UseFetchResult<T> {
  data: T | null
  loading: boolean
  error: string | null
  refetch: () => void
}
 
function useFetch<T>(url: string): UseFetchResult<T> {
  const [state, setState] = useState<AsyncState<T>>({ status: 'idle' })
 
  const fetch_ = useCallback(async () => {
    setState({ status: 'loading' })
    try {
      const res = await fetch(url)
      if (!res.ok) throw new Error(`HTTP ${res.status}`)
      const data = await res.json() as T
      setState({ status: 'success', data })
    } catch (e) {
      setState({ status: 'error', error: e instanceof Error ? e.message : 'Unknown error' })
    }
  }, [url])
 
  useEffect(() => { fetch_() }, [fetch_])
 
  return {
    data: state.status === 'success' ? state.data : null,
    loading: state.status === 'loading',
    error: state.status === 'error' ? state.error : null,
    refetch: fetch_,
  }
}

Avoid These Patterns

as Casts Are Type Lies

// This compiles but crashes at runtime if the element isn't an input
const input = document.getElementById('search') as HTMLInputElement
input.value = 'hello' // Crashes if element is a div
 
// Better: use a type guard
const element = document.getElementById('search')
if (element instanceof HTMLInputElement) {
  element.value = 'hello'  // TypeScript knows it's HTMLInputElement here
}

Non-Null Assertions Without Justification

// This compiles but crashes if user is undefined
const username = user!.name
 
// Better: handle the undefined case
const username = user?.name ?? 'Anonymous'

any in API Responses

// BAD — you lose all type safety after this fetch
const data: any = await fetch('/api/users').then(r => r.json())
 
// GOOD — validate and type the response
import { z } from 'zod'
 
const UserSchema = z.object({
  id: z.string(),
  name: z.string(),
  email: z.string().email(),
})
 
const UsersSchema = z.array(UserSchema)
type User = z.infer<typeof UserSchema>
 
const raw = await fetch('/api/users').then(r => r.json())
const users = UsersSchema.parse(raw)  // Throws if shape doesn't match
// users is typed as User[] with guaranteed shape

Utility Types Worth Knowing

// Pick — select specific fields
type UserPreview = Pick<User, 'id' | 'name' | 'avatar'>
 
// Omit — exclude specific fields
type UserWithoutPassword = Omit<User, 'password' | 'salt'>
 
// Partial — all fields optional (useful for update payloads)
type UpdateUserInput = Partial<Omit<User, 'id' | 'createdAt'>>
 
// Required — all fields required
type RequiredConfig = Required<Config>
 
// Record — object with specific key/value types
const statusMessages: Record<'active' | 'inactive' | 'banned', string> = {
  active: 'Your account is active',
  inactive: 'Your account is inactive',
  banned: 'Your account has been banned',
}
 
// ReturnType — extract return type from a function
type FetchResult = ReturnType<typeof fetchUser>

Key Takeaways

  • Discriminated unions model state more accurately than optional fields
  • Generic components eliminate duplicate type definitions
  • Never use any for API responses — validate with Zod and infer types from the schema
  • as casts and ! operators are red flags — use type guards instead
  • Return types on custom hooks make them easier to use and harder to misuse
  • TypeScript's utility types (Pick, Omit, Partial, Record) cover most common type transformations
Popular Blogs
Claude AI vs ChatGPT: An Honest Comparison for Developers
  • April 28, 2026
AI Tools Every Developer Should Be Using in 2026
  • April 20, 2026
Using the Claude API in Real Projects: A Practical Developer Guide
  • April 15, 2026
Prompt Engineering for Developers: Write Prompts That Actually Work
  • April 10, 2026
Categories
AIDevOpsNext.jsMobile DevelopmentWeb Development

Related Posts

Web Development
Building REST APIs with Node.js and Express: The Complete Guide
Sabir Khaloufi·Dec 25, 2025
Web Development
React Performance Optimization Techniques That Actually Work
Sabir Khaloufi·Dec 20, 2025
Web Development
Understanding JWT Authentication in Modern Web Apps
Sabir Khaloufi·Dec 10, 2025