Saltar al contenido principal
Volver al blogCopiar Markdown
·Actualizado ·8 min de lectura
·typescriptjavascriptbest practices

TypeScript estricto: los flags y tipos que cazan bugs

Un tsconfig.json estricto, con noUncheckedIndexedAccess, y tipos avanzados cazan errores antes de producción. El setup, los tipos clave y dónde molesta. →

Óscar Gallego

Ingeniero de software · Málaga

Escudo de seguridad de tipos de TypeScript protegiendo el código de bugs
En este artículo

Respuesta corta: strict: true no es la meta. El flag que casi todas las configs estrictas siguen dejando fuera es noUncheckedIndexedAccess, que tipa arr[i] como T | undefined. Lanza npx tsc --noEmit --noUncheckedIndexedAccess y cuenta los errores: cada uno es un sitio que hoy puede petar en ejecución. El resto del post es el setup que yo usaría y qué categoría de error caza cada pieza.

TypeScript no es solo “JavaScript con tipos”. Es un sistema de prevención de errores que cobra desde el primer día, pero solo si lo configuras para que de verdad diga que no. No he medido cuántos bugs evita en un proyecto concreto, así que no te pongo una cifra: te cuento qué caza cada flag.

Las cinco prácticas que hacen el trabajo

Todo el post se reduce a una configuración estricta más un uso avanzado del sistema de tipos, para que los errores mueran en tiempo de compilación y no en producción.

  • Activa el modo estricto: habilita todos los flags de strict en tu tsconfig.json para la máxima seguridad.
  • Usa noUncheckedIndexedAccess: evita errores de undefined al acceder a arrays y objetos.
  • Diferencia type e interface: type para uniones y tipos complejos, interface para objetos y APIs públicas que pueden extenderse.
  • Domina los utility types: Awaited, Parameters, ReturnType, Extract y Exclude manipulan tipos para que no los repitas.
  • Aplica type narrowing: type guards, uniones discriminadas y la palabra clave asserts refinan los tipos y guían al compilador.

Un tsconfig.json estricto es tu primera línea de defensa

La mayoría de los desarrolladores no activan todos los flags estrictos. Ese es el error caro.

Los flags fundamentales

La base de un setup que aguanta.

{
  "compilerOptions": {
    "strict": true,
    "strictNullChecks": true,
    "strictFunctionTypes": true,
    "strictBindCallApply": true,
    "strictPropertyInitialization": true,
    "noImplicitThis": true,
    "alwaysStrict": true
  }
}

Los flags que cazan lo raro

Ve más allá de lo básico y más bugs mueren en tiempo de compilación.

{
  "compilerOptions": {
    // ... flags fundamentales
    "noUncheckedIndexedAccess": true,    // Crítico
    "noImplicitOverride": true,           // Evita bugs en herencia
    "noImplicitReturns": true,            // Fuerza returns explícitos
    "noFallthroughCasesInSwitch": true,   // Atrapa bugs en switch
    "noUnusedLocals": true,               // Limpia código muerto
    "noUnusedParameters": true,           // Detecta parámetros no usados
    "exactOptionalPropertyTypes": true,   // Diferencia undefined de ausente
    "noPropertyAccessFromIndexSignature": true // Fuerza notación de corchetes
  }
}

Configuración para tooling moderno

Esto te mantiene compatible con Vite, Astro y Next.js.

{
  "compilerOptions": {
    // ... otros flags
    "module": "ESNext",
    "moduleResolution": "bundler",
    "resolveJsonModule": true,
    "allowImportingTsExtensions": true,
    "noEmit": true,
    "esModuleInterop": true,
    "allowSyntheticDefaultImports": true,
    "isolatedModules": true,
    "target": "ES2022",
    "lib": ["ES2023", "DOM", "DOM.Iterable"],
    "skipLibCheck": true,
    "forceConsistentCasingInFileNames": true
  }
}

noUncheckedIndexedAccess: un flag que desmiente al compilador

Sin este flag, se asume que acceder a un índice de un array siempre es seguro. Esa suposición es una fuente común de errores en tiempo de ejecución.

Con noUncheckedIndexedAccess: false (el valor por defecto), users[10] tiene el tipo string, lo cual es una mentira. Con noUncheckedIndexedAccess: true, users[10] tiene el tipo string | undefined, lo cual es la verdad.

Este único flag te obliga a añadir verificaciones en cada acceso por índice y saca a la luz los sitios donde el código daba por hecho que el elemento existe.

Aquí va la parte honesta. “Obliga” es la palabra exacta: el flag molesta exactamente tantas veces como tu código estaba mal, y esa factura la pagas por adelantado, en cada acceso indexado, antes de haber publicado nada. Y lo que encuentra son bugs potenciales, no crashes en producción confirmados; en la sección de más abajo tienes el recuento real sobre esta web. La estrictez es fricción por diseño. Cambias una tarde de quejas del compilador por el undefined is not a function de las 2 de la mañana. Yo me quedo con el compilador.

type vs interface: la regla que cierra el debate

La regla general: interface para la forma de objetos y para APIs públicas (porque puede extenderse), type para todo lo demás (uniones, primitivos, tipos complejos).

La comparación directa:

Característicainterfacetype
Ideal paraEstructuras de objetos (OOP), APIs públicasUniones, primitivos, tipos complejos, funciones
ExtensiónSí, con extends y declaration mergingNo directamente; se logra con intersecciones (&)
Declaration MergingSí (permite añadir nuevos campos)No (genera un error de duplicado)
Uniones y PrimitivosNo, no se puede usar para string | number o stringSí (type ID = string | number;)
TuplasNoSí (type Point = [number, number];)
Mapped TypesNoSí (type Readonly<T> = ...)

La versión corta

  • Usa interface cuando:
    • Defines la “forma” de un objeto o una clase.
    • Quieres que los usuarios de tu API puedan extender la definición (ej. plugins).
  • Usa type cuando:
    • Necesitas uniones, tuplas o tipos de función.
    • Necesitas tipos complejos construidos con mapped types o condicionales.

Utility types que se ganan el sueldo

Awaited<T>

Desenvuelve el tipo de una Promise. Esencial para inferir el tipo de retorno de funciones asíncronas.

async function fetchUser() {
  return { id: '1', name: 'Alice' };
}
type User = Awaited<ReturnType<typeof fetchUser>>;
// User es { id: string; name: string }

Parameters<T> y ReturnType<T>

Extraen los tipos de los parámetros y del retorno de una función. Perfectos para wrappers y decoradores.

function createUser(name: string, age: number) { /* ... */ }
type CreateUserParams = Parameters<typeof createUser>; // [string, number]

Extract<T, U> y Exclude<T, U>

Filtran tipos de una unión basándose en una condición.

type Event =
  | { type: 'click'; x: number; y: number }
  | { type: 'keypress'; key: string };

// Extrae solo el evento de click
type MouseEvent = Extract<Event, { x: number }>;

Narrowing: dile al compilador lo que tú ya sabes

Type guards definidos por el usuario

Una función que devuelve un booleano y le señala un tipo a TypeScript.

function isCat(animal: Animal): animal is Cat {
  return animal.type === 'cat';
}

Uniones discriminadas

Una propiedad común (como status o type) convierte tus estados en una máquina que TypeScript puede seguir.

type LoadingState =
  | { status: 'loading' }
  | { status: 'success'; data: string };

function handleState(state: LoadingState) {
  if (state.status === 'success') {
    console.log(state.data); // TS sabe que `data` existe
  }
}

La palabra clave asserts

Una función que lanza un error si una condición de tipo no se cumple, afirmando el tipo para el resto del bloque.

function assertIsString(value: unknown): asserts value is string {
  if (typeof value !== 'string') {
    throw new Error('No es un string');
  }
}

Esta web va en strict, pero sin noUncheckedIndexedAccess

Dar consejos es fácil, así que el 6 de octubre de 2026 he hecho las mismas comprobaciones sobre el código de este blog (TypeScript 6.0.3).

Extiende astro/tsconfigs/strict, que añade strict encima de la config base de Astro y ningún otro flag de rigor. O sea, sin noUncheckedIndexedAccess, el flag que te acabo de decir que actives. astro check sale limpio en 128 ficheros: 0 errores, 0 warnings, 0 hints. Al activar el flag saltan 35 errores en 13 ficheros. 22 están en scripts de usar y tirar dentro de scripts/; los otros 13, en 8 ficheros de src/, todos accesos por índice (un elemento de un array o una entrada de un objeto) que el código da por hecho que existen.

any explícitos: 10 líneas en 4 ficheros. Seis están en los helpers throttle/debounce/rafThrottle: tres restricciones (...args: any[]) => any y tres this: any. Las otras cuatro son un adaptador de cookies con tipos flojos (dos), el lookup de i18n y un Record<string, any> en las props de Markdown.

Ese es el tamaño de mi hueco, por escrito: 35 errores que he aceptado encontrarme en ejecución y cuatro any que son deuda sin más. Si quieres tu propio número, son dos comandos: el tsc de arriba y grep -rn ': any\|as any' src | wc -l.

Te toca

Así que abre tu tsconfig.json y cuenta cuántos flags de este post te faltan. Cada uno es una categoría de bug que has aceptado encontrar en producción.

Lectura relacionada: los tipos estrictos cazan lo que tu portátil perdona, y la CI es la otra mitad de esa historia. Mira por qué los tests pasan en local y fallan en Vercel.


P.D. ¿Tienes una historia de terror con un flag estricto, o un flag que te parece sobrevalorado? Cuéntamelo en LinkedIn.

Última actualización:

Comparte este artículo

Artículos relacionados