Buenas Prácticas en TypeScript: Reduce 90% los Bugs
Un tsconfig.json estricto y tipos avanzados bajaron nuestros bugs en producción de 47 a 3. El setup, los tipos que hicieron el trabajo y dónde dolió. →
Óscar Gallego
Desarrollador Web
En este artículo
Hace 2 años heredé un proyecto con 80,000 líneas de JavaScript puro. El marcador: 47 bugs en producción en 3 meses.
Después de migrar a TypeScript con una configuración estricta, ese número bajó a 3 bugs en 6 meses. De cuarenta y siete a tres.
TypeScript no es solo “JavaScript con tipos”. Es un sistema de prevención de errores que empieza a pagar desde el primer día, pero solo si lo configuras para que de verdad diga que no.
Las cinco prácticas que hicieron 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
stricten tutsconfig.jsonpara la máxima seguridad. - Usa
noUncheckedIndexedAccess: evita errores deundefinedal acceder a arrays y objetos. - Diferencia
typeeinterface:typepara uniones y tipos complejos,interfacepara objetos y APIs públicas que pueden extenderse. - Domina los utility types:
Awaited,Parameters,ReturnType,ExtractyExcludemanipulan tipos para que no los repitas. - Aplica type narrowing: type guards, uniones discriminadas y la palabra clave
assertsrefinan 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 robusto.
{
"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, 23 bugs
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 tipostring, lo cual es una mentira. ConnoUncheckedIndexedAccess: true,users[10]tiene el tipostring | undefined, lo cual es la verdad.
Este único flag me obligó a añadir verificaciones adecuadas y descubrió 23 bugs potenciales en un proyecto existente.
Aquí va la parte honesta. “Obligó” 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 son bugs potenciales, no 23 crashes en producción. 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ística | interface | type |
|---|---|---|
| Ideal para | Estructuras de objetos (OOP), APIs públicas | Uniones, primitivos, tipos complejos, funciones |
| Extensión | Sí, con extends y declaration merging | No directamente; se logra con intersecciones (&) |
| Declaration Merging | Sí (permite añadir nuevos campos) | No (genera un error de duplicado) |
| Uniones y Primitivos | No, no se puede usar para string | number o string | Sí (type ID = string | number;) |
| Tuplas | No | Sí (type Point = [number, number];) |
| Mapped Types | No | Sí (type Readonly<T> = ...) |
La versión corta
- Usa
interfacecuando:- Defines la “forma” de un objeto o una clase.
- Quieres que los usuarios de tu API puedan extender la definición (ej. plugins).
- Usa
typecuando:- 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');
}
}
Cinco años después: los números
Tras 5 años usando TypeScript en entornos de producción:
- Bugs en runtime: reducidos en un 85%.
- Tiempo de refactorización: 60% más rápido gracias a la seguridad de tipos.
- Onboarding de desarrolladores: 40% más rápido porque el código se autodocumenta.
Cada hora invertida en configurar tipos correctamente ahorra diez horas de depuración futura.
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 Twitter/X.
Artículos relacionados


Debug Astro: Arregla "Campos de Schema Ocultos" en 5 Min
