Inicio / TypeScript / React: Frontend Moderno con TypeScript / TypeScript con React

TypeScript con React

Tipado de props, eventos, hooks, componentes genéricos y forwardRef.

Avanzado Web
🔒 Solo lectura
📖

Estás en modo lectura

Puedes leer toda la lección, pero para marcar progreso, hacer ejercicios y ganar XP necesitas una cuenta Pro.

Desbloquear por $9/mes

TypeScript con React

TypeScript es el estándar de la industria para proyectos React profesionales. Proporciona detección de errores en tiempo de desarrollo, autocompletado y documentación implícita a través de tipos.


Tipos básicos de componentes

Componente con props tipadas

interface ButtonProps {
  label: string
  variant?: 'primary' | 'secondary' | 'danger'
  disabled?: boolean
  onClick: () => void
}

function Button({ label, variant = 'primary', disabled = false, onClick }: ButtonProps) {
  return (
    <button className={`btn btn-${variant}`} disabled={disabled} onClick={onClick}>
      {label}
    </button>
  )
}

Props con children

// React.ReactNode: acepta cualquier cosa renderizable
interface CardProps {
  title: string
  children: React.ReactNode
}

// React.PropsWithChildren: shortcut para agregar children a tus props
type CardProps2 = React.PropsWithChildren<{
  title: string
}>

Tipos de eventos

function EventExamples() {
  // Evento de clic
  const handleClick = (e: React.MouseEvent<HTMLButtonElement>) => {
    console.log(e.currentTarget.textContent)
  }

  // Evento de cambio en input
  const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
    console.log(e.target.value)
  }

  // Evento de envío de formulario
  const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
    e.preventDefault()
    const formData = new FormData(e.currentTarget)
  }

  // Evento de teclado
  const handleKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
    if (e.key === 'Enter') console.log('Enter presionado')
  }

  return (
    <form onSubmit={handleSubmit}>
      <input onChange={handleChange} onKeyDown={handleKeyDown} />
      <button onClick={handleClick}>Enviar</button>
    </form>
  )
}

Tipado de hooks

useState

// Inferido automáticamente
const [count, setCount] = useState(0)           // number
const [name, setName] = useState('Ana')          // string
const [active, setActive] = useState(true)       // boolean

// Explícito cuando el tipo inicial no es suficiente
const [user, setUser] = useState<User | null>(null)
const [items, setItems] = useState<Item[]>([])
const [status, setStatus] = useState<'idle' | 'loading' | 'error'>('idle')

useRef

// Ref al DOM (inicializado como null, no mutable por nosotros)
const inputRef = useRef<HTMLInputElement>(null)
const divRef = useRef<HTMLDivElement>(null)

// Ref mutable (almacenar valor sin re-render)
const timerRef = useRef<number | null>(null)
const countRef = useRef(0) // inferido como MutableRefObject<number>

useReducer

interface State {
  count: number
  error: string | null
}

type Action =
  | { type: 'increment'; payload: number }
  | { type: 'decrement'; payload: number }
  | { type: 'reset' }
  | { type: 'error'; payload: string }

function reducer(state: State, action: Action): State {
  // TypeScript sabe exactamente qué payload tiene cada action
  switch (action.type) {
    case 'increment':
      return { ...state, count: state.count + action.payload }
    case 'error':
      return { ...state, error: action.payload }
    case 'reset':
      return { count: 0, error: null }
    default:
      return state
  }
}

Componentes genéricos

interface ListProps<T> {
  items: T[]
  renderItem: (item: T) => React.ReactNode
  keyExtractor: (item: T) => string | number
  emptyMessage?: string
}

function List<T>({ items, renderItem, keyExtractor, emptyMessage = 'Sin resultados' }: ListProps<T>) {
  if (items.length === 0) return <p>{emptyMessage}</p>

  return (
    <ul>
      {items.map(item => (
        <li key={keyExtractor(item)}>
          {renderItem(item)}
        </li>
      ))}
    </ul>
  )
}

// Uso — T se infiere automáticamente
<List
  items={users}
  keyExtractor={u => u.id}
  renderItem={u => <span>{u.name} ({u.email})</span>}
/>

Select genérico

interface SelectProps<T> {
  options: T[]
  value: T | null
  onChange: (value: T) => void
  getLabel: (option: T) => string
  getValue: (option: T) => string
}

function Select<T>({ options, value, onChange, getLabel, getValue }: SelectProps<T>) {
  return (
    <select
      value={value ? getValue(value) : ''}
      onChange={e => {
        const selected = options.find(o => getValue(o) === e.target.value)
        if (selected) onChange(selected)
      }}
    >
      <option value="">Seleccionar...</option>
      {options.map(option => (
        <option key={getValue(option)} value={getValue(option)}>
          {getLabel(option)}
        </option>
      ))}
    </select>
  )
}

Extender elementos HTML nativos

// Heredar todas las props nativas de <button>
interface ButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
  variant?: 'primary' | 'secondary'
  loading?: boolean
}

function Button({ variant = 'primary', loading, children, ...rest }: ButtonProps) {
  return (
    <button className={`btn btn-${variant}`} disabled={loading} {...rest}>
      {loading ? 'Cargando...' : children}
    </button>
  )
}

// Ahora acepta TODAS las props nativas de <button>:
<Button onClick={handleClick} type="submit" variant="primary" aria-label="Guardar">
  Guardar
</Button>

Con forwardRef

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

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

Input.displayName = 'Input'

Tipos discriminados (Discriminated Unions)

Perfectos para componentes con variantes:

type AlertProps =
  | { variant: 'success'; message: string }
  | { variant: 'error'; message: string; onRetry: () => void }
  | { variant: 'loading' }

function Alert(props: AlertProps) {
  switch (props.variant) {
    case 'success':
      return <div className="alert-success">✅ {props.message}</div>
    case 'error':
      return (
        <div className="alert-error">
          ❌ {props.message}
          <button onClick={props.onRetry}>Reintentar</button>
        </div>
      )
    case 'loading':
      return <div className="alert-loading">⏳ Cargando...</div>
  }
}

// TypeScript fuerza las props correctas:
<Alert variant="success" message="Guardado" />           // ✅
<Alert variant="error" message="Falló" onRetry={retry} /> // ✅
<Alert variant="error" message="Falló" />                 // ❌ Falta onRetry
<Alert variant="loading" />                               // ✅

Tipos útiles de React

Tipo Uso
React.ReactNode Cualquier cosa renderizable
React.ReactElement Solo elementos JSX
React.FC<Props> Tipo de componente funcional (controversia: algunos lo evitan)
React.CSSProperties Objeto de estilos inline
React.ComponentProps<typeof MyComp> Extraer props de un componente
React.ComponentProps<'button'> Props nativas de un elemento HTML
React.PropsWithChildren<P> Agrega children: ReactNode a P

Resumen

Concepto Ejemplo
Props tipadas interface Props { name: string }
Eventos React.MouseEvent<HTMLButtonElement>
useState con tipo useState<User | null>(null)
Componente genérico function List<T>({ items }: { items: T[] })
Extender HTML extends React.ButtonHTMLAttributes<HTMLButtonElement>
Discriminated unions Props que cambian según variant
forwardRef tipado React.forwardRef<HTMLInputElement, Props>
🔒

Ejercicio práctico disponible

Sistema de tipos avanzado para componentes React

Desbloquear ejercicios
// Sistema de tipos avanzado para componentes React
// Desbloquea Pro para acceder a este ejercicio
// y ganar +50 XP al completarlo

function ejemplo() {
    // Tu código aquí...
}

¿Te gustó esta lección?

Con Pro puedes marcar progreso, hacer ejercicios, tomar quizzes, ganar XP y obtener tu constancia.

Ver planes desde $9/mes