Mejores prácticas de TypeScript + React
Un resumen condensado de las 25 mejores prácticas más importantes extraídas de cada página en esta sección.
- Habilita modo estricto más flags adicionales:
strict: truees una familia de flags pero no incluyenoUncheckedIndexedAccessoexactOptionalPropertyTypes— actívalos explícitamente para que el acceso con corchetes devuelvaT | undefinedy{ theme: undefined }no sea silenciosamente permitido en props opcionales. - Usa jsx: preserve, no jsx: react: Establece
"jsx": "preserve"(o"react-jsx") y"moduleResolution": "bundler"para que no tengas que hacerimport Reacten cada archivo y para que las condiciones deexportsen package.json se resuelvan correctamente. - Exporta desde archivos declare global: Un archivo
.d.tsusandodeclare global { … }solo participa en la fusión de módulos si el archivo en sí se trata como un módulo; agrega unexport {}al final o la ampliación global desaparece silenciosamente. - Valida variables env en tiempo de ejecución: Aumentar
ProcessEnven un.d.tsproporciona autocompletado en tiempo de compilación pero cero garantía en tiempo de ejecución, así que analizaprocess.enva través de un esquema Zod (o un auxiliarrequireEnv) al inicio para que las variables faltantes fallen rápidamente. - Cambia en props.variant, no desestructures: El estrechamiento de unión discriminada depende de la ruta
props.variant, así que desestructurarconst { variant } = propsrompe el enlace y quiebra el estrechamiento; cambia directamente enprops.variantpara mantener los campos de cada caso tipados. - Usa assertNever para cambios exhaustivos: Termina cambios de unión discriminada con
default: return assertNever(action)—function assertNever(x: never): never { throw new Error("Unexpected: " + x) }— así agregar una nueva variante falla la verificación de tipos en lugar de pasar silenciosamente. - Usa
<T,>en genéricos de flecha: En archivos.tsx, los genéricos de función flecha chocan con la sintaxis JSX, así que escribe<T,>(coma al final) o<T extends unknown>para desambiguar el parámetro genérico de una etiqueta de elemento. - Evita React.FC para genéricos y async:
React.FCno puede llevar un parámetro de tipo genérico y no puede ser async, así que escribefunction List<T>(props: …)oasync function Page()como declaraciones de función simple para componentes genéricos y Server Components. - Trata cada aserción non-null ! como un bug: Bajo verificaciones de null estrictas, cada
value!es unTypeErrorpotencial en tiempo de ejecución, así que prefiere encadenamiento opcional, retornos tempranos, o una función de aserción personalizada que realmente lance un error con un mensaje que puedas depurar. - Desestructura tuplas para mantener sus tipos:
noUncheckedIndexedAccesstambién se aplica a tuplas vía acceso con corchetes, así quetuple[0]se convierte enT | undefined; desestructurarconst [a, b] = tuplepreserva los tipos de elementos conocidos sin la unión. - Prefiere satisfies sobre anotaciones de tipo:
const routes = { home: "/", about: "/about" } satisfies Record<string, string>valida forma sin ampliar tipos literales, así que las claves mantienen sus tipos exactos"home" | "about"para búsquedas posteriores — una anotación simple: Record<string, string>descarta eso. - Las funciones de aserción deben lanzar: Una función tipada con
asserts x is Testrecha todo el código posterior en la llamada, pero TypeScript te confía — si la función devuelvefalseen lugar de lanzar, cada tipo posterior es silenciosamente incorrecto. - Analiza con Zod las respuestas de la API:
response.json()devuelvePromise<any>y el castingas Tno proporciona protección en tiempo de ejecución; canaliza respuestas a través de un esquema Zod —const user = UserSchema.parse(await res.json())— para que la validación y el tipo provengan de una única fuente de verdad. - Verifica response.ok explícitamente:
fetchno lanza en 4xx o 5xx — solo rechaza en fallo de red — así que siempre ramifica enresponse.oky trata los que no son 2xx como errores antes de llamar a.json(). - Contexto con default null más hook de guarda: Prefiere
createContext<T | null>(null)con un hook de guarda —function useAuth() { const ctx = useContext(AuthCtx); if (!ctx) throw new Error("Missing AuthProvider"); return ctx }— para que los bugs de proveedor faltante salgan a la superficie inmediatamente en lugar de chocar en el primer acceso a propiedad. - Prefiere event.currentTarget sobre event.target:
event.targetse tipea como el amplioEventTarget, mientras queevent.currentTargetlleva el tipo de elemento genérico (HTMLInputElement,HTMLFormElement), así que usacurrentTargetpara.value,.select(), onew FormData(...). - Extiende props del DOM con ComponentPropsWithoutRef: Tipea componentes envoltorios con
React.ComponentPropsWithoutRef<"input">(oWithRefcuando reenvíes) para que cada atributo nativo se mantenga sincronizado con el DOM automáticamente, en lugar de redeclarardisabled,onChange, etc. - children es ReactNode, no JSX.Element:
JSX.Elementse estrecha a un solo elemento y rechaza strings, números, arrays, ynull; usaReact.ReactNodeparachildrenpara que los consumidores puedan pasar cualquier contenido renderizable sin luchar con el tipo. - Tipea refs de temporizador con ReturnType:
setIntervaldevuelve unnumberen navegadores y un objetoTimeouten Node —const timer = useRef<ReturnType<typeof setInterval> | null>(null)— así que este patrón mantiene el código portátil entre SSR y el cliente. - Espera params en Next.js 15: Los manejadores de ruta y props de página ahora reciben
paramsysearchParamscomo Promesas, así queconst { id } = await context.params— desestructurar sinawaitte deja con un objeto Promise que silenciosamente se convierte en cadena a"[object Promise]". - satisfies tus respuestas JSON: Usa
NextResponse.json({ error: "not found" } satisfies ApiErrorResponse)para que el valor de retorno debe coincidir con tu contrato de respuesta sin ampliar; envuelverequest.json()(tipadoPromise<any>) en validación Zod para que los clientes no puedan enviar cargas útiles arbitrarias. - Solo props serializables cruzan el límite: Los valores de prop de Servidor→Cliente deben ser strings, números, objetos planos,
Date,FormData, arrays tipados, o Server Actions; funciones, instancias de clase,Map/Set, ySymbolno pueden cruzar, y pasarlos lanza un error de serialización en el renderizado. - Server Components pueden ser async, Client no puede: Los componentes de función async y el
awaitdirecto son legales solo en Server Components — un componente Client escrito comoasync functioncompila pero lanza en tiempo de ejecución, y TypeScript no lo marca, así que aplica la división manualmente o con reglas de lint. - Siempre inicializa useState:
useState<User>()sin valor inicial se amplía aUser | undefinedsin advertencia — usauseState<User | null>(null)para ser explícito, o proporciona un valor inicial; el setter reemplaza el state completamente, no se fusiona como classsetState. - Omit no da error en claves malas:
Omit<T, "nonExistent">silenciosamente devuelve el tipo completo porque TypeScript amplía el parámetro clave, así que los typos pasan desapercibidos — también recuerda quePartialyReadonlyson superficiales, así que usa unDeepPartial/DeepReadonlypersonalizado para actualizaciones anidadas.