TypeScript con React 19: los cambios en el tipado que afectan a tu código

React 19 salió en versión estable en diciembre de 2024 y, con él, llegaron cambios en @types/react que rompen código que antes funcionaba sin problema. No son cambios enormes, pero si migras un proyecto existente o empiezas uno nuevo con React 19 y TypeScript, conviene saber qué esperar. Aquí tienes los puntos que más afectan al día a día.

children ya no viene gratis con React.FC

Antes, declarar un componente así era perfectamente válido:

const MyComponent: React.FC = () => <div />;

Y dentro del componente podías acceder a children sin declararla. Eso se acabó. En @types/react v19, React.FC no incluye children de forma implícita. Si tu componente la necesita, tienes que pedirla explícitamente:

const MyComponent = ({ children }: { children: React.ReactNode }) => {
  return <div>{children}</div>;
};

El motivo tiene sentido: muchos componentes usaban React.FC sin aceptar children en absoluto, pero TypeScript no protestaba porque el tipo las incluía igualmente. Ahora el contrato es explícito: si declaras que aceptas children, es porque las vas a usar.

React.FC sigue siendo útil para indicar que algo es un componente de función, pero ya no te regala nada extra en el tipado.

ref como prop, sin forwardRef

Pasar un ref de padre a hijo antes requería envolver el componente en React.forwardRef:

// React 18 y anterior
const Input = React.forwardRef<HTMLInputElement, { placeholder: string }>(
  ({ placeholder }, ref) => <input ref={ref} placeholder={placeholder} />
);
Input.displayName = 'Input';

En React 19, ref es una prop como cualquier otra. Puedes escribirlo así:

// React 19
function Input({ ref, ...props }: { ref?: React.Ref<HTMLInputElement>; placeholder: string }) {
  return <input ref={ref} {...props} />;
}

forwardRef sigue existiendo en los tipos de v19 y no se va a romper de golpe, pero ya no hace falta para la mayoría de casos. Si estás migrando, puedes ir reescribiéndolo poco a poco: no es urgente, pero el nuevo patrón es más limpio.

Server Components: qué cambia (y qué no) en los tipos

Los Server Components llegaron con Next.js 13 y llevan un tiempo con nosotros, pero hay una confusión frecuente: @types/react por sí solo no distingue entre server y client components. Esa lógica la añade Next.js con sus propios tipos adicionales.

Lo que sí reflejan los tipos de React 19 es que ciertos hooks (useState, useEffect, event handlers como onClick) no tienen sentido en un Server Component. Si intentas usarlos sin el marcador 'use client', Next.js te va a dar error en tiempo de compilación, aunque sea por su propio sistema de tipos, no por @types/react.

La convención práctica es sencilla: si el archivo tiene 'use client' al principio, TypeScript trata el componente como client component con acceso a hooks y eventos. Sin él, es un Server Component y solo puedes usar código asíncrono y sin estado del lado del cliente.

Los nuevos hooks y cómo están tipados

React 19 añade varios hooks nuevos. Estos son los que más vas a usar con TypeScript:

useFormStatus

import { useFormStatus } from 'react-dom';

function SubmitButton() {
  const { pending, data, method, action } = useFormStatus();
  return <button disabled={pending}>{pending ? 'Enviando...' : 'Enviar'}</button>;
}

El tipo que devuelve es { pending: boolean; data: FormData | null; method: string | null; action: string | function | null }. Lo más útil en la práctica es pending para deshabilitar el botón mientras se procesa el formulario.

useActionState

Antes se llamaba useFormState y está renombrado en React 19. Si ves errores del tipo "useFormState no existe", es esto:

import { useActionState } from 'react';

async function submitForm(prevState: { error?: string } | null, formData: FormData) {
  // lógica del servidor
  return { error: 'Algo falló' };
}

function MyForm() {
  const [state, dispatch] = useActionState(submitForm, null);
  return (
    <form action={dispatch}>
      {state?.error && <p>{state.error}</p>}
      <button type="submit">Enviar</button>
    </form>
  );
}

TypeScript infiere el tipo del estado a partir del tipo de retorno de la función que le pases. Si devuelves { error?: string }, state será de tipo { error?: string } | null.

useOptimistic

const [optimisticMessages, addOptimisticMessage] = useOptimistic(
  messages,
  (state: Message[], newMessage: string) => [...state, { text: newMessage, sending: true }]
);

El tipo del estado optimista se deriva del tipo del estado original y de la función de actualización que le pases. No hace falta anotar nada extra si los tipos son concretos.

use()

import { use } from 'react';

function UserName({ userPromise }: { userPromise: Promise<{ name: string }> }) {
  const user = use(userPromise);
  return <span>{user.name}</span>;
}

use() acepta una Promise<T> o un Context<T> y devuelve el valor tipado correctamente. Es la forma de leer el resultado de una promesa dentro del render, suspendiendo el componente hasta que esté lista.

Tipar Server Actions

Las Server Actions son funciones asíncronas que se ejecutan en el servidor. Su firma en TypeScript suele verse así:

async function createPost(formData: FormData): Promise<{ error?: string }> {
  'use server';
  // lógica de servidor
  return {};
}

El tipo de retorno siempre es Promise<T> o void. Si las usas con useActionState, el tipo de state coincide exactamente con lo que devuelva la action, así que basta con tipar bien la función en el servidor y el cliente lo recibe ya tipado.

Para validación de datos en el servidor, TypeScript y el ecosistema React en 2025 cubre cómo encaja Zod en este flujo.

@types/react v18 vs v19: qué versión tienes

Mucha gente instala React 19 pero se queda con @types/react@18 porque npm no actualiza los tipos de tipo @types automáticamente. Para verificar:

npm list @types/react

Si ves @types/[email protected] con React 19 instalado, actualiza:

npm install @types/react@19 @types/react-dom@19

Con las versiones desincronizadas puede que algunos errores no aparezcan (o aparezcan donde no deberían), porque los tipos de v18 no reflejan los cambios de v19 en React.FC, forwardRef o los nuevos hooks.

Errores habituales al migrar

  • "Property 'children' does not exist on type '{}'": tu componente usa React.FC o un tipo de props sin declarar children. Añade children?: React.ReactNode a las props.
  • forwardRef con warnings de deprecación: no está roto todavía, pero puedes migrar a ref como prop cuando tengas un momento.
  • "useFormState is not a function": renombrado a useActionState. Actualiza la importación.
  • useRef con tipo incorrecto: useRef<HTMLInputElement>(null) sigue igual, pero recuerda que .current es HTMLInputElement | null, no HTMLInputElement. Si sabes que el ref siempre estará montado cuando lo uses, puedes hacer el assert en el punto de uso.

tRPC v11 y React 19: tipado de extremo a extremo

Si usas tRPC en un proyecto con Next.js 15 y React 19, la versión 11 de tRPC (publicada en 2025) es la que toca. La integración con server actions es nativa y los tipos del backend llegan al cliente sin configuración extra: defines el router en el servidor, llamas al procedimiento desde el cliente, y TypeScript sabe exactamente qué devuelve.

// cliente
const { data } = trpc.post.list.useQuery();
// data está tipado con lo que devuelva el router de posts

Con useMutation la dinámica es la misma: el tipo de los argumentos y el tipo de la respuesta vienen del backend. Si cambias la firma del procedimiento en el servidor, el cliente rompe en tiempo de compilación. Para proyectos donde el tipado de la API es importante, JavaScript y React: la base del tipado moderno da contexto sobre por qué este enfoque funciona bien a largo plazo.

Conclusión

Los cambios de @types/react v19 no son dramáticos, pero sí concretos: children explícito en las props, ref como prop normal, nuevos hooks con tipos bien definidos. Si tienes un proyecto existente con React 18, la migración principal es añadir children?: React.ReactNode donde haga falta y actualizar useFormState a useActionState. El resto puede ir al ritmo que quieras.

Imagen: Pexels / panumas nikhomkhai

COMPARTE ESTE ARTÍCULO

COMPARTIR EN FACEBOOK
COMPARTIR EN TWITTER
COMPARTIR EN LINKEDIN
COMPARTIR EN WHATSAPP