TypeScript strict mode: las 6 opciones del tsconfig que más impactan en producción y cuándo activarlas
Hay una escena que se repite. Alguien configura un proyecto nuevo, le dice a todo el mundo "usamos TypeScript estricto" y agrega strict: true al tsconfig.json. Todos asienten. El CI compila. Y tres meses después aparece un bug en producción que TypeScript podría haber atrapado si hubieran activado noUncheckedIndexedAccess.
Mi tesis es directa: strict: true es un atajo cómodo que activa seis flags razonables pero deja afuera dos opciones que, en mi criterio, previenen más bugs silenciosos que la mitad del grupo base. El problema no es strict: true en sí — es que la mayoría lo activa y siente que ya terminó.
Este post no es "activá strict y listo". Es un análisis bandera por bandera: qué hace cada una, qué tipo de error previene y cuál es el orden sensato para migrar una codebase que todavía no las tiene todas activadas.
Qué incluye strict: true — y qué no
Según la documentación oficial de TypeScript, strict: true es un shorthand que activa este conjunto de flags:
strictNullChecksstrictFunctionTypesstrictBindCallApplystrictPropertyInitializationnoImplicitAnynoImplicitThisuseUnknownInCatchVariables(desde TypeScript 4.4)alwaysStrict(emite"use strict"en el output JS)
Lo que no activa por defecto:
noUncheckedIndexedAccessexactOptionalPropertyTypesnoImplicitOverridenoPropertyAccessFromIndexSignature
Ese segundo grupo no vive bajo el paraguas de strict. Son flags independientes que TypeScript eligió no incluir porque generan muchos errores nuevos en codebases existentes. Eso no significa que sean opcionales para producción — significa que los diseñadores tomaron una decisión conservadora. Vos podés elegir diferente.
Las 6 opciones con mayor impacto real
1. strictNullChecks — la más importante del grupo base
Sin esto, null y undefined son asignables a cualquier tipo. Con esto activado:
// Sin strictNullChecks: compila sin error
function getUsername(user: User): string {
return user.name; // user podría ser null
}
// Con strictNullChecks: el compilador exige que manejés el caso
function getUsername(user: User | null): string {
if (!user) throw new Error("Usuario no encontrado");
return user.name;
}Si tenés que elegir un único flag para activar hoy, es este. La mayoría de los crashes en runtime de aplicaciones TypeScript que no lo tienen activado tienen una firma común: Cannot read properties of undefined.
No hay discusión acá. Si no tenés strictNullChecks, no tenés TypeScript — tenés JavaScript con tipado cosmético.
2. noImplicitAny — el segundo prioritario
Cuando TypeScript no puede inferir el tipo de algo y vos no lo declaraste, tiene dos opciones: error o any silencioso. Sin este flag, elige any silencioso.
// Sin noImplicitAny: compila. 'data' es any implícito.
function process(data) {
return data.toUpperCase(); // sin chequeo
}
// Con noImplicitAny: error. Tenés que declarar el tipo.
function process(data: string): string {
return data.toUpperCase();
}El any implícito es como un agujero en el sistema de tipos. No lo ves, no te avisa, y se propaga. noImplicitAny lo cierra.
3. strictFunctionTypes — para quienes trabajan con callbacks y genéricos
Este flag hace que TypeScript verifique los tipos de los parámetros de funciones de forma contravariante (en lugar de bivariante). Es el más técnico del grupo y el que menos gente entiende, pero importa cuando pasás callbacks entre capas de la aplicación.
type Handler = (event: MouseEvent) => void;
// Sin strictFunctionTypes: esto compila aunque es inseguro
const handler: Handler = (event: Event) => {
console.log((event as MouseEvent).clientX); // cast manual, riesgo
};
// Con strictFunctionTypes: error. MouseEvent no es assignable a Event en posición de parámetro.En una codebase de React con muchos event handlers, este flag atrapa asignaciones de función que parecen razonables pero esconden pérdidas de tipo en runtime.
4. useUnknownInCatchVariables — el underrated del grupo base
Antes de TypeScript 4.4, el error en un bloque catch era any. Con este flag activado, es unknown, lo que te fuerza a verificar su tipo antes de usarlo.
try {
await fetchData();
} catch (error) {
// Sin useUnknownInCatchVariables: error es 'any'
// Con useUnknownInCatchVariables: error es 'unknown'
if (error instanceof Error) {
// Ahora sí podés acceder a error.message con seguridad
console.error(error.message);
} else {
console.error("Error desconocido", error);
}
}En sistemas donde el manejo de errores importa — autenticación, integraciones externas, procesamiento de pagos — este flag previene que asumas la forma del error sin validarla. Lo activa strict: true desde TS 4.4, pero vale la pena entender por qué existe.
5. noUncheckedIndexedAccess — el que más bugs previene fuera del grupo base
Este es el que no activa strict: true y el que más debería importarte. Cuando accedés a un array por índice o a un objeto por clave string, TypeScript por defecto asume que el valor existe. Con noUncheckedIndexedAccess, el tipo retornado incluye | undefined.
// tsconfig: noUncheckedIndexedAccess: true
const items = ["primero", "segundo", "tercero"];
const item = items[5];
// Sin noUncheckedIndexedAccess: item es 'string'
// Con noUncheckedIndexedAccess: item es 'string | undefined'
// Ahora el compilador te fuerza a chequearlo antes de usarlo:
if (item !== undefined) {
console.log(item.toUpperCase()); // ✅
}
// Sin el chequeo: error de compilación
// console.log(item.toUpperCase()); // ❌ Object is possibly 'undefined'El mismo comportamiento aplica para index signatures:
const map: Record<string, number> = { a: 1 };
const value = map["b"];
// Sin noUncheckedIndexedAccess: value es 'number'
// Con noUncheckedIndexedAccess: value es 'number | undefined'La documentación oficial es clara en esto. ¿Por qué no está en strict? Porque genera muchos errores en codebases existentes donde el acceso por índice es ubicuo y nadie lo valida. Pero eso no lo hace opcional si querés cobertura real.
En escenarios con Prisma y resultados de queries, con respuestas de APIs externas casteadas a arrays, o con configuraciones leídas de JSON — este flag atrapa exactamente la clase de bug que aparece tarde, en producción, cuando el array llega vacío por primera vez.
6. exactOptionalPropertyTypes — el más subvalorado de todos
Este es el segundo que más gente ignora y el que más sutilmente rompe cosas. Sin este flag, TypeScript trata undefined como un valor válido para una propiedad opcional. Con él, hay una diferencia entre "la propiedad puede no estar" y "la propiedad está y vale undefined".
interface Config {
timeout?: number; // propiedad opcional
}
// Sin exactOptionalPropertyTypes:
// Estas dos asignaciones son equivalentes para TypeScript:
const a: Config = {}; // timeout no existe
const b: Config = { timeout: undefined }; // timeout existe pero es undefined
// Con exactOptionalPropertyTypes:
const c: Config = { timeout: undefined }; // ❌ Error
// Type 'undefined' is not assignable to type 'number'
// porque 'timeout?' significa 'puede no estar', no 'puede ser undefined'¿Por qué importa? Porque hay una diferencia operacional entre una clave ausente y una clave con valor undefined. En serialización JSON, en spreads de objetos, en Prisma updates — el comportamiento difiere. exactOptionalPropertyTypes hace que TypeScript entienda esa diferencia.
El orden para migrar una codebase existente
Si estás agregando esto a un proyecto que ya tiene código, el orden sensato es:
Paso 1: strictNullChecks → más errores, más impacto, pero son los más urgentes
Paso 2: noImplicitAny → segundo lote de errores, más fáciles de resolver
Paso 3: strict: true → activa el resto del grupo base de golpe
Paso 4: noUncheckedIndexedAccess → errores nuevos, pero son exactamente los que quería ver
Paso 5: exactOptionalPropertyTypes → último, requiere entender bien el modelo de datos
Una estrategia útil para proyectos grandes es activar los flags con // @ts-expect-error de forma temporal y resolverlos archivo por archivo. Otra es usar skipLibCheck: true mientras migrás para no quedar bloqueado por tipos de dependencias que todavía no fueron actualizadas.
// tsconfig.json — configuración de migración progresiva
{
"compilerOptions": {
// Paso 1: empezá por acá
"strictNullChecks": true,
// Paso 2: una vez que el proyecto compila con el anterior
"noImplicitAny": true,
// Paso 3: activa el grupo base completo
"strict": true,
// Paso 4 y 5: después de estabilizar el grupo base
"noUncheckedIndexedAccess": true,
"exactOptionalPropertyTypes": true,
// Temporal durante la migración:
"skipLibCheck": true
}
}Los errores que más se cometen al migrar
Activar todo de una y abandonar. El CI explota con 400 errores y alguien decide que "TypeScript strict es demasiado restrictivo". El problema no es el flag — es el orden.
Usar as para silenciar en lugar de corregir. Cada as unknown as TipoQueQuiero es una deuda de tipos. Patea el error a runtime y hace que la migración sea cosmética.
// Esto no es una migración, es un disfraz:
const result = fetchUser() as User; // ❌ Ignora que fetchUser puede retornar null
// Esto sí:
const raw = await fetchUser();
if (!raw) throw new Error("Usuario no encontrado");
const result: User = raw; // ✅Ignorar los dos flags fuera de strict. Este es el error más común y el que motivó este post. Muchos equipos declaran TypeScript estricto sin saber que noUncheckedIndexedAccess no está incluido en ese preset.
Activar exactOptionalPropertyTypes sin revisar los updates de Prisma. En Prisma, los updates usan propiedades opcionales extensamente. Con este flag, hay patrones que antes compilaban y dejan de hacerlo. No es un bloqueo — es una señal de que había un modelo de datos impreciso. Pero conviene saber que el lote de errores va a aparecer ahí.
Qué no podés concluir solo con esto
Este análisis se basa en la documentación oficial y en patrones conocidos de TypeScript. Lo que no podés inferir de acá:
- Cuántos errores va a generar en tu codebase específica. Eso solo lo sabés corriendo
tsc --noEmitcon cada flag activado. - Si
exactOptionalPropertyTypesvale el costo en un proyecto con Prisma v5 sin refactors previos. Puede ser mucho trabajo por valor marginal si el modelo de datos está bien tipado de otra forma. - Si hay incompatibilidades con librerías de terceros que no soportan bien
noUncheckedIndexedAccess.skipLibCheck: truemitiga esto, pero no lo elimina.
La decisión de cuándo activar cada flag requiere correr el compilador en el propio código y leer los errores. No hay atajos acá.
FAQ
¿strict: true activa noUncheckedIndexedAccess?
No. strict: true es un preset que activa ocho flags específicos documentados en la referencia oficial. noUncheckedIndexedAccess no es uno de ellos. Hay que activarlo por separado en el tsconfig.json.
¿Cuál es el primer flag que debería activar si mi proyecto no tiene ninguno?
strictNullChecks. Es el que previene la mayor clase de errores en runtime y es el prerequisito lógico para que el resto de los flags tenga sentido. Sin chequeos de null, los otros flags son decoración.
¿noImplicitAny rompe el uso de any explícito?
No. noImplicitAny solo penaliza el any que TypeScript infiere cuando no puede determinar el tipo. Si escribís any explícito (const x: any = ...), compila igual. Eso es intencional: a veces necesitás escaparte del sistema de tipos. Pero al menos lo hacés conscientemente.
¿Puedo activar estos flags progresivamente en un monorepo?
Sí. Cada paquete del monorepo puede tener su propio tsconfig.json que extiende una base compartida. Una estrategia común es activar los flags más estrictos en los paquetes nuevos y migrar los viejos de forma incremental. El riesgo es que los tipos que cruzan paquetes pueden quedar en zonas grises durante la transición.
¿exactOptionalPropertyTypes rompe los spreads de objetos?
Puede hacerlo si estás usando spreads para pasar propiedades opcionales con valor undefined. El compilador va a marcar esos casos porque hay una diferencia semántica entre propiedad ausente y propiedad con valor undefined. En la mayoría de los casos, el fix es usar narrowing o spreads condicionales en lugar de asumir que undefined pasa transparentemente.
¿Vale la pena activar todo esto en un proyecto que ya funciona?
Depende del costo de los bugs que querés prevenir. Si el sistema maneja autenticación, datos financieros o cualquier tipo de información donde un error silencioso tiene consecuencias reales — sí, vale la pena el costo de la migración. Si es un prototipo interno que no llega a usuarios — quizás strict: true alcanza por ahora. El criterio es el costo del error, no la comodidad del setup. Este tema conecta directamente con decisiones de arquitectura más amplias, del tipo de las que aparecen en el post sobre arquitectura backend de identidad digital: los flags no son decoración, son parte del contrato de seguridad del sistema.
Mi postura y el próximo paso concreto
strict: true es el piso, no el techo. El preset existe para que la adopción sea fácil, no para que la conversación termine ahí.
Los dos flags que más impacto tienen fuera del grupo base son noUncheckedIndexedAccess y exactOptionalPropertyTypes. El primero cierra la puerta a la clase de error más común en acceso a arrays y maps. El segundo hace que el modelo de tipos refleje la diferencia real entre "propiedad ausente" y "propiedad con valor undefined" — una distinción que importa en serialización, en Prisma y en cualquier código que recibe datos del exterior.
Lo que no compro es la actitud de "activé strict, ya está". Es la misma energía que agrega un healthcheck que solo verifica que el proceso responde — da una sensación de seguridad que no mide lo que creés que mide.
El próximo paso concreto: corré tsc --noEmit con noUncheckedIndexedAccess: true en el proyecto donde estás trabajando ahora. Leé los errores. Si son manejables, activalo. Si son 200+ errores, empezá por los archivos más críticos. No necesitás resolver todo de una — necesitás saber qué ignorabas.
Fuentes originales:
- TypeScript Strict Mode Docs: https://www.typescriptlang.org/tsconfig#strict
- TypeScript noUncheckedIndexedAccess: https://www.typescriptlang.org/tsconfig#noUncheckedIndexedAccess
Artículos Relacionados
Arquitectura backend de identidad digital: las decisiones que los tutoriales omiten
Los tutoriales de auth muestran el happy path. Los problemas reales de identidad digital aparecen en la revocación, la propagación de cambios de estado y el modelo de confianza. Una guía de decisiones desde adentro.
System prompts para agentes en producción: el formato que sobrevivió 3 rediseños
Un system prompt no es documentación para el modelo: es un contrato. Después de varios rediseños, llegué a un formato con secciones fijas, límites explícitos y contexto inyectado dinámicamente. Esto es lo que quedó y por qué.
Docker healthchecks: qué miden de verdad y qué no deberías prometer
Un healthcheck que solo dice "el proceso responde" puede esconder fallas de negocio graves. Analizamos qué promete de verdad la instrucción HEALTHCHECK, dónde falla la receta estándar y cómo usarla como señal operativa limitada, no como garantía de salud.
Comentarios (0)
¿Qué pensás de esto?
Dejá tu comentario en 10 segundos.
Usamos tu login solo para mostrar tu nombre y avatar. Nada de spam.