Back

React & TypeScript: Patrones Comunes para un Código Más Limpio

React & TypeScript: Patrones Comunes para un Código Más Limpio

Al construir aplicaciones React con TypeScript, te encontrarás repetidamente con los mismos desafíos: cómo tipar las props correctamente, manejar eventos de forma segura y estructurar componentes para máxima reutilización. Estos patrones no se tratan solo de satisfacer al compilador—se tratan de escribir código que sea más fácil de entender, refactorizar y mantener.

Este artículo cubre los patrones más prácticos de React TypeScript que usarás diariamente. Aprenderás cómo tipar componentes efectivamente, manejar props opcionales, trabajar con eventos y refs, aprovechar tipos utilitarios y usar uniones discriminadas en reducers. Cada patrón incluye ejemplos enfocados que muestran exactamente por qué mejora tu código.

Puntos Clave

  • Tipa props y state explícitamente usando interfaces o types para mejor IntelliSense y detección de errores
  • Maneja props opcionales y por defecto con propiedades opcionales de TypeScript y parámetros por defecto
  • Usa tipos utilitarios como Pick y ComponentProps para reducir repetición y mantener consistencia
  • Tipa eventos y refs apropiadamente para interacciones DOM type-safe
  • Aprovecha uniones discriminadas en reducers para manejo exhaustivo de acciones
  • Compón componentes con forwardRef para mantener type safety al pasar refs

Tipado de Props y State

La base de los patrones React TypeScript comienza con tipar correctamente las props de tus componentes. Esto detecta errores en tiempo de compilación y proporciona mejor autocompletado en tu editor.

Patrón Básico de Props

interface ButtonProps {
  label: string
  onClick: () => void
  disabled?: boolean
}

function Button({ label, onClick, disabled = false }: ButtonProps) {
  return (
    <button onClick={onClick} disabled={disabled}>
      {label}
    </button>
  )
}

Este patrón asegura que tus componentes reciban los tipos de datos correctos. La interfaz documenta claramente qué props espera el componente, haciéndolo autodocumentado.

Tipado de State con useState

TypeScript usualmente infiere los tipos de state correctamente, pero el state complejo se beneficia del tipado explícito:

import { useState } from 'react'

interface User {
  id: string
  name: string
  email: string
}

function UserProfile() {
  const [user, setUser] = useState<User | null>(null)
  const [loading, setLoading] = useState(false) // Tipo inferido como boolean

  // TypeScript sabe que user podría ser null
  return user ? <div>{user.name}</div> : <div>Loading...</div>
}

Manejo de Props Opcionales y por Defecto

Los componentes React a menudo tienen props de configuración opcionales. Las propiedades opcionales de TypeScript combinadas con parámetros por defecto crean un patrón limpio:

interface CardProps {
  title: string
  description?: string
  variant?: 'primary' | 'secondary'
  className?: string
}

function Card({ 
  title, 
  description, 
  variant = 'primary',
  className = ''
}: CardProps) {
  return (
    <div className={`card card-${variant} ${className}`}>
      <h2>{title}</h2>
      {description && <p>{description}</p>}
    </div>
  )
}

Este patrón hace que los componentes sean flexibles mientras mantiene type safety. Las props requeridas son claras, y las props opcionales tienen valores por defecto sensatos.

Tipado de Eventos y Refs

Los manejadores de eventos y refs son fuentes comunes de errores de TypeScript. Estos patrones eliminan las conjeturas:

Patrón de Manejador de Eventos

import { useState, ChangeEvent, FormEvent } from 'react'

interface FormProps {
  onSubmit: (data: { email: string; password: string }) => void
}

function LoginForm({ onSubmit }: FormProps) {
  const [email, setEmail] = useState('')
  const [password, setPassword] = useState('')

  const handleEmailChange = (e: ChangeEvent<HTMLInputElement>) => {
    setEmail(e.target.value)
  }

  const handleSubmit = (e: FormEvent<HTMLFormElement>) => {
    e.preventDefault()
    onSubmit({ email, password })
  }

  return (
    <form onSubmit={handleSubmit}>
      <input type="email" value={email} onChange={handleEmailChange} />
      <input type="password" value={password} onChange={(e) => setPassword(e.target.value)} />
      <button type="submit">Login</button>
    </form>
  )
}

Patrón de Ref

import { useRef, useEffect } from 'react'

function AutoFocusInput() {
  const inputRef = useRef<HTMLInputElement>(null)

  useEffect(() => {
    // TypeScript sabe que inputRef.current podría ser null
    inputRef.current?.focus()
  }, [])

  return <input ref={inputRef} type="text" />
}

Uso de Tipos Utilitarios

Los tipos utilitarios de TypeScript reducen la duplicación de código y mantienen los tipos sincronizados a través de tu aplicación.

Patrón ComponentProps

Extrae props de componentes existentes sin repetir definiciones de tipos:

import { ComponentProps } from 'react'

// Obtén todas las props que acepta un elemento button
type ButtonProps = ComponentProps<'button'> & {
  variant?: 'primary' | 'secondary'
}

function StyledButton({ variant = 'primary', ...props }: ButtonProps) {
  return <button className={`btn-${variant}`} {...props} />
}

// Extrae props de tus propios componentes
type MyButtonProps = ComponentProps<typeof StyledButton>

Patrón Pick

Selecciona props específicas al extender componentes:

interface UserData {
  id: string
  name: string
  email: string
  role: string
  lastLogin: Date
}

// Solo necesita algunas propiedades para la tarjeta
type UserCardProps = Pick<UserData, 'name' | 'email'> & {
  onClick?: () => void
}

function UserCard({ name, email, onClick }: UserCardProps) {
  return (
    <div onClick={onClick}>
      <h3>{name}</h3>
      <p>{email}</p>
    </div>
  )
}

Composición de Componentes con forwardRef

Al construir librerías de componentes o envolver elementos nativos, forwardRef mantiene type safety mientras expone refs:

import { forwardRef, useRef, InputHTMLAttributes } from 'react'

interface InputProps extends InputHTMLAttributes<HTMLInputElement> {
  label: string
  error?: string
}

const Input = forwardRef<HTMLInputElement, InputProps>(
  ({ label, error, ...props }, ref) => {
    return (
      <div>
        <label>{label}</label>
        <input ref={ref} {...props} />
        {error && <span className="error">{error}</span>}
      </div>
    )
  }
)

Input.displayName = 'Input'

// El uso mantiene type safety completo
function Form() {
  const inputRef = useRef<HTMLInputElement>(null)
  
  return <Input ref={inputRef} label="Email" type="email" />
}

Uniones Discriminadas en Reducers

Las uniones discriminadas hacen que las acciones de reducer sean type-safe y permiten verificación exhaustiva:

interface TodoState {
  items: Array<{ id: string; text: string; done: boolean }>
  filter: 'all' | 'active' | 'completed'
}

type TodoAction =
  | { type: 'ADD_TODO'; payload: string }
  | { type: 'TOGGLE_TODO'; payload: string }
  | { type: 'SET_FILTER'; payload: TodoState['filter'] }
  | { type: 'CLEAR_COMPLETED' }

function todoReducer(state: TodoState, action: TodoAction): TodoState {
  switch (action.type) {
    case 'ADD_TODO':
      return {
        ...state,
        items: [...state.items, {
          id: Date.now().toString(),
          text: action.payload,
          done: false
        }]
      }
    
    case 'TOGGLE_TODO':
      return {
        ...state,
        items: state.items.map(item =>
          item.id === action.payload
            ? { ...item, done: !item.done }
            : item
        )
      }
    
    case 'SET_FILTER':
      return { ...state, filter: action.payload }
    
    case 'CLEAR_COMPLETED':
      return {
        ...state,
        items: state.items.filter(item => !item.done)
      }
    
    default:
      // TypeScript asegura que todos los casos sean manejados
      const exhaustive: never = action
      return state
  }
}

Este patrón asegura que manejes todos los tipos de acción. Si agregas un nuevo tipo de acción, TypeScript dará error hasta que lo manejes en el reducer.

Actualizaciones de React 19

Con React 19, las nuevas capacidades como Actions (useActionState, useFormStatus), la API use() para consumir promesas o contexto, y soporte de primera clase para Server Components hacen que el type safety sea aún más importante. Al tipar Actions, define explícitamente la forma de FormData y el valor de retorno de la acción para que tanto el código del servidor como del cliente se mantengan sincronizados. El próximo cambio de forwardRef a pasar ref como prop estándar también simplifica el tipado—trata ref como cualquier otra prop y deja que TypeScript asegure la corrección a través de los límites de componentes.

Conclusión

Estos patrones comunes de React TypeScript forman la base de aplicaciones React type-safe. Al tipar props y state explícitamente, manejar eventos y refs correctamente, y aprovechar los tipos utilitarios de TypeScript, creas código que es tanto más seguro como más mantenible. Los patrones trabajan juntos—el tipado apropiado de props permite mejor composición de componentes, mientras que las uniones discriminadas aseguran manejo exhaustivo en lógica de state compleja.

Comienza con tipado básico de props e incorpora gradualmente tipos utilitarios y patrones avanzados conforme tu aplicación crezca. La inversión en tipado apropiado se compensa a través de mejor soporte del IDE, menos errores en tiempo de ejecución y contratos de componentes más claros.

Preguntas Frecuentes

Usa interface cuando podrías necesitar extender o fusionar declaraciones más tarde. Usa type para uniones, intersecciones, o cuando trabajas con tipos utilitarios. Ambos funcionan bien para props básicas, así que elige un enfoque y mantente consistente.

Usa ReactNode para máxima flexibilidad: children: ReactNode. Esto acepta strings, números, elementos, fragmentos y arrays. Para casos más específicos, usa ReactElement o tipos de elementos específicos.

Usa funciones genéricas que preserven los tipos de props del componente original mientras agregan nuevos. El tipo utilitario ComponentProps ayuda a extraer y extender tipos de componentes existentes sin repetición.

Tipa parámetros de función, tipos de retorno para funciones exportadas, y state complejo explícitamente. Deja que TypeScript infiera casos simples como useState con primitivos o manejadores de eventos obvios para reducir verbosidad.

Listen to your bugs 🧘, with OpenReplay

See how users use your app and resolve issues fast.
Loved by thousands of developers