TypeScript en React
Un padre renombra una prop de word a currentWord. Tres hijos siguen leyendo word, y nada lo señala: la app compila, la página carga, y el primer usuario que hace clic en el botón que renderiza uno de esos hijos ve una pantalla en blanco. TypeScript agrega una capa de tipos sobre JavaScript: cada valor tiene un tipo declarado o inferido, y el compilador señala el código que no coincide antes de que se ejecute.
En React eso paga exactamente donde se esconden errores como ese, en los puntos donde los datos se mueven entre componentes. Una prop tipada es un contrato: el padre debe enviar la forma correcta, el hijo puede confiar en lo que llega, y tu editor autocompletará ambos lados.
Este capítulo cubre las partes específicas de React: tipar state, props, children, function props y valores de retorno de componentes. Asume que ya conoces lo básico de TypeScript, unions, tipos personalizados y genéricos; la sección abre con lecciones de repaso sobre exactamente eso, y luego lo pone todo en práctica reconstruyendo el juego Assembly: Endgame en TypeScript.
Configuración inicial: plantilla react-ts de Vite
Vite incluye una versión TypeScript de su plantilla de React. Una bandera en el momento de crear el proyecto te da un proyecto completamente configurado:
npm create vite@latest my-react-app -- --template react-tsLos archivos de componente usan la extensión .tsx en lugar de .jsx (TypeScript más JSX), y módulos simples sin JSX usan .ts en lugar de .js. Todo lo demás sobre la configuración funciona como siempre; Vite compila los tipos y envía JavaScript simple al navegador.
Una trampa viene con eso: los errores de tipo aparecen en tu editor y fallan npm run build, que ejecuta tsc -b antes de agrupar, pero el servidor de desarrollo elimina las anotaciones sin verificarlas. npm run dev servirá un proyecto lleno de errores de tipo, así que una app que se ejecuta no es prueba de que los tipos pasen.
Tipar state
useState generalmente se tipa a sí mismo. El hook infiere su tipo del valor inicial que pasas, así que el state creado con un valor inicial real no necesita anotación en absoluto:
const [currentWord, setCurrentWord] = useState(getRandomWord())Si getRandomWord retorna un string, currentWord es un string y setCurrentWord solo acepta strings. Llama a setCurrentWord(true) y TypeScript lo señala al instante: un boolean no es asignable donde se espera un string. Esa es la inferencia haciendo su trabajo, y para la mayoría del state puedes dejarla sola.
La inferencia falla cuando el valor inicial está vacío o es demasiado amplio para describir el futuro del state. Un array vacío no dice nada sobre qué contendrá, así que useState([]) infiere never[], un array cuyos elementos nunca pueden ser nada, lo que hace que cada push sea un error. Aquí es donde recurres a la forma explícita: useState es una función genérica, y pasas el argumento de tipo entre llaves angulares.
const [guessedLetters, setGuessedLetters] = useState<string[]>([])Ahora el state es un array de strings aunque el valor inicial sea un array vacío, y tanto el valor como el setter lo refuerzan. El mismo movimiento maneja state nullable, donde el valor comienza como null y solo después se convierte en algo real:
type Word = { text: string; difficulty: number }
const [selectedWord, setSelectedWord] = useState<Word | null>(null)La union Word | null le dice a TypeScript ambas formas que este state puede legalmente contener. Cada lectura de selectedWord ahora te obliga a manejar el caso null antes de tocar .text, lo que convierte un crash clásico en tiempo de ejecución en un empujón en tiempo de compilación. La mecánica del state en sí es inalterada; TypeScript solo fija qué está permitido que el state contenga.
Tipar props de componentes
Las props llegan como un objeto, así que tiparlas significa anotar ese objeto. Para un componente con una o dos props, una anotación inline funciona:
function ConfettiContainer({ isGameWon }: { isGameWon: boolean }) {
// ...
}Los tipos inline se vuelven desordenados a medida que las props se multiplican. El patrón estándar es un type alias nombrado, convencionalmente llamado ComponentNameProps, declarado encima del componente:
type GameStatusProps = {
isGameWon: boolean
wrongGuessCount: number
message?: string
}
function GameStatus({ isGameWon, wrongGuessCount, message }: GameStatusProps) {
// ...
}El ? en message la marca como opcional: los padres pueden omitirla, y dentro del componente su tipo es string | undefined. Una declaración interface GameStatusProps { ... } funciona exactamente en la misma posición; para tipar props las dos son intercambiables, y el curso se mantiene con type aliases por consistencia con los tipos personalizados que construye en otros lugares.
Si un componente acepta children, tipalo como ReactNode, el tipo amplio que cubre todo lo que React puede renderizar: elementos, strings, números, fragments, arrays de todos estos.
import { type ReactNode } from 'react'
type CardProps = {
title: string
children: ReactNode
}Cómo fluyen los children a través de un componente se cubre en Children y composition; ReactNode es el tipo que los describe. Todo sobre cómo se comportan las props en tiempo de ejecución permanece igual. La anotación de tipo agrega el contrato encima.
Tipar function props
Los componentes a menudo reciben funciones como props: un click handler, un callback que reporta un valor hacia arriba. El tipo para una function prop usa sintaxis de flecha: la lista de parámetros con sus tipos, una flecha, y el tipo de retorno.
type NewGameButtonProps = {
startNewGame: () => void
}
type LetterButtonProps = {
letter: string
onGuess: (value: string) => void
}() => void describe una función que no toma argumentos y no retorna nada, la forma de la mayoría de los handlers de estilo evento. (value: string) => void dice que el hijo llamará a la función con un string, así que la implementación del padre debe aceptar uno. Pasa un handler cuyos tipos de parámetro no coincidan y el error aparece en el call site JSX, en el padre, en tiempo de compilación.
Sin embargo, la verificación hace dos concesiones, y ambas parecen el verificador de tipos dejando algo pasar. Un handler puede declarar menos parámetros que los que el tipo lista, así que onGuess={() => setOpen(true)} satisface (value: string) => void. Y () => void acepta una función que retorna un valor, así que startNewGame={async () => { await saveScore() }} type-checkea aunque devuelva una promesa que nada espera. El compilador detecta tipos de parámetro incorrectos en lugar de cada diferencia en la forma.
Tipos de retorno y valores derivados
Pasa el mouse sobre un componente function y TypeScript ya sabe que retorna React.JSX.Element, inferido del JSX en la declaración de retorno. (El namespace JSX vive dentro del módulo react ahora en lugar de ser global, así que escribir la anotación a mano significa importarla.)
Puedes dejar esa inferencia sola, y muchos codebases lo hacen. Anotar el tipo de retorno es una decisión sobre strictness: garantiza que el componente siempre retorna un elemento único, lo que algunos equipos valoran en codebases más grandes. Eso es una promesa más estricta que la que React hace, ya que un componente legalmente puede retornar un string, un número, un array, o null, y una anotación JSX.Element simple rechaza todos esos hasta que los amplíes.
import { type JSX } from 'react'
function Header(): JSX.Element {
return <h1>Assembly: Endgame</h1>
}
function ConfettiContainer({ isGameWon }: { isGameWon: boolean }): JSX.Element | null {
if (!isGameWon) return null
return <Confetti />
}ConfettiContainer a veces no renderiza nada retornando null, el mismo movimiento usado en renderizado condicional, así que su tipo de retorno anotado es la union JSX.Element | null. El mismo hábito se extiende a los valores derivados dentro del componente: el tipo de retorno de una arrow function se añade después de sus paréntesis de parámetros, y un map sobre datos que produce JSX produce un JSX.Element[]. La mayoría de estas anotaciones reafirman lo que la inferencia ya concluyó, así que trátalas como documentación opcional. Los que consistentemente ganan su espacio se sientan en los límites: props, state vacío o nullable, y funciones cuyas firmas otros componentes dependen.
Importar tipos compartidos
Los tipos se exportan e importan como cualquier otro binding, lo que mantiene una definición sirviendo a toda la app. Los módulos de datos comúnmente exportan su forma junto con los datos:
// languages.ts
export type Language = {
name: string
backgroundColor: string
color: string
}
// LanguageChips.tsx
import { type Language } from './languages'
type LanguageChipsProps = {
languages: Language[]
}La palabra clave type en la importación la marca como type-only, así que desaparece completamente del JavaScript compilado. Una vez que un tipo como Language vive en un lugar, cada componente que toca esos datos importa el mismo contrato, y cambiar la forma en un archivo superficie cada call site que necesita actualización.
El state creado con un valor inicial real se etiqueta a sí mismo, y cuando el valor inicial está vacío o null escribes la etiqueta tú mismo, como useState<string[]>([]). Las props obtienen un pequeño tipo nombrado como GameStatusProps listando cada prop y su tipo.
Una vez que las etiquetas están puestas, tu editor te advierte el momento en que algo de la forma equivocada llega, antes de que incluso ejecutes la app.
Ese es el manual. Lo que comenzó como una función que retorna un poco de markup es ahora un toolkit completo: componentes y state, effects y datos, patrones de componentes reutilizables, routing, un modelo de renderizado para razonar sobre performance, y una capa de tipos que detecta errores en los límites antes de que alguien más los encuentre. Elige un proyecto que realmente quieras que exista, créalo con npm create vite@latest, y abre estos capítulos de nuevo cuando el proyecto comience a pedirlos.

