Ir al contenido
Web

TypeScript strict mode: los 9 ajustes que nunca apagamos

strict: true activa 9 comprobaciones y desde TypeScript 6.0 es el default. Qué atrapa cada flag en producción, más los 3 flags fuera del bundle que añadimos.

25 de mayo de 2026 · 10 min de lectura · Actualizado el 7 de octubre de 2026

On an amaranth ground, nine dark concentric rings around a white core, nine dark lines arriving from outside, each stopped at a different ring

TypeScript strict mode es un único flag dentro de tsconfig.json que activa nueve comprobaciones separadas del compilador a la vez. Las nueve son strictNullChecks, noImplicitAny, strictFunctionTypes, strictBindCallApply, strictPropertyInitialization, noImplicitThis, useUnknownInCatchVariables, alwaysStrict y strictBuiltinIteratorReturn, que entró en la familia con TypeScript 5.6. La referencia TSConfig de Microsoft las lista en la familia strict, y desde TypeScript 6.0, publicado en marzo de 2026, strict está activado por defecto: la issue 62333 se cerró contra la milestone 6.0.0, así que hoy un proyecto tiene que apagar strict a propósito para volver al comportamiento laxo.

La mayoría de los proyectos arranca con strict: true en la raíz y un puñado de archivos o módulos que en silencio renuncian a una o dos de las comprobaciones. Esas renuncias casi nunca vuelven atrás. Este artículo es la lista de las nueve, con el fallo que cada una previene y el precio que hemos pagado por dejarla puesta. Ninguna es gratis. Ninguna nos ha costado más que el bug que atrapó.

Por qué el bundle importa

El flag strict es el único contrato estable. Si un equipo desactiva una de las nueve, cada lector del código tiene ahora que recordar cuál se ha apagado. Cada dependencia tipada contra strict gana un agujero más. Cada desarrollador junior que entra al equipo redescubre el mismo problema. El bundle no es una checklist; es una declaración pública sobre cómo un proyecto razona sobre los tipos. Una vez que un flag está apagado, los otros lo siguen despacio. Desde 6.0 esa declaración se hace por omisión: strict es el default, así que cada renuncia es ya una línea puesta a propósito en la configuración.

Las 9 comprobaciones dentro de strict

1. strictNullChecks

Qué atrapa. null y undefined como tipos distintos de cualquier otro tipo. Con el flag apagado, una función que devuelve User devuelve implícitamente User | null | undefined, y cada dereferencia es insegura.

El coste. Cada valor de una respuesta de API, cada prop opcional, cada return de Array.prototype.find() necesita un narrowing.

Por qué la dejamos puesta. Cada proyecto que hemos heredado con strictNullChecks: false envía un bug Cannot read properties of undefined a producción al menos una vez por trimestre. El coste en runtime de uno de esos crashes supera el coste en desarrollo de un if en varios órdenes de magnitud. Es la feature de sistema de tipos más importante de TypeScript, y el consejo de migración de la comunidad es constante: actívala la última porque produce el mayor número de errores, y no vuelvas a apagarla.

2. noImplicitAny

Qué atrapa. Parámetros y valores de retorno sin anotación y no inferibles. Con el flag apagado, esas posiciones se convierten en silencio en any.

El coste. Anotaciones extra en helpers rápidos, sobre todo durante la migración desde JavaScript.

Por qué la dejamos puesta. any es un virus. Un solo any en un helper profundo borra los tipos en todos los llamadores, a menudo sin aviso. Cada equipo que he visto desactivar noImplicitAny "para la migración" un año más tarde tenía la mayoría del código nuevo sin tipar. El flag es el único mecanismo que obliga al equipo a una decisión consciente cuando un tipo es desconocido, en lugar de dejarse caer en any por inercia.

3. strictFunctionTypes

Qué atrapa. Violaciones de contravarianza en parámetros de función. Una función que recibe un Dog no puede asignarse a una variable que espera una función que recibe cualquier Animal, porque el tipo más amplio la llamaría con valores que la función más estrecha no sabe manejar.

El coste. Fricción ocasional con tipos de librerías escritos antes de la existencia de este flag. El flag tampoco se aplica a los parámetros en sintaxis método, así que los casos más ruidosos se quedan callados.

Por qué la dejamos puesta. Los bugs de callback son silenciosos en runtime. Un handler que espera el subtipo equivocado se llama con valores que no sabe manejar y o bien crashea muy adentro de un render o bien hace en silencio la cosa equivocada. En nuestro propio código nunca hemos tenido un error strictFunctionTypes que resultara ser un falso positivo.

4. strictBindCallApply

Qué atrapa. Argumentos equivocados pasados a .bind(), .call() o .apply().

El coste. Casi cero en proyectos modernos, porque .bind() ya nadie lo escribe.

Por qué la dejamos puesta. El flag es gratis en código que usa arrow functions, métodos de clase y hooks de React. Apagarla solo importaría para código legacy que de todos modos habría que modernizar. No hay ninguna ventaja en desactivarla.

5. strictPropertyInitialization

Qué atrapa. Campos de clase declarados con un tipo distinto de undefined pero nunca asignados en el constructor. Sin esta comprobación, el campo está en silencio en undefined en la primera lectura.

El coste. Los campos hay que inicializarlos en el constructor, marcarlos con el operador de asignación definida (!) o tipar para incluir undefined.

Por qué la dejamos puesta. La alternativa son campos que parecen inicializados en el tipo pero no lo están en runtime, y que aparecen como bugs en el setup de tests y en los contenedores de DI. El operador ! es la salida para los casos en que un framework garantiza la inicialización, y la obligación de escribirlo hace visible la suposición. La comprobación se paga sola en cualquier proyecto que use controladores de clase, entidades ORM u objetos de servicio.

6. noImplicitThis

Qué atrapa. Expresiones this cuyo tipo el compilador no puede inferir, típicamente en callbacks pasados a forEach o en funciones aisladas usadas como event handlers.

El coste. Cero en código moderno de React y Node que usa arrow functions y clases ES. Real en código legacy con muchas clases, que es justo donde atrapa más bugs.

Por qué la dejamos puesta. Un this mal vinculado es una de las familias de bugs más viejas de JavaScript. Si TypeScript la atrapa gratis, no hay argumento para renunciar.

7. useUnknownInCatchVariables

Qué atrapa. El err dentro de un bloque catch (err). Con este flag err está tipado como unknown en lugar de any, así que no puedes dereferenciarlo sin hacer primero un narrowing.

El coste. Una línea if (err instanceof Error) por bloque catch.

Por qué la dejamos puesta. any dentro de un catch es una de las formas más comunes en que any se cuela en el resto del código. err.message termina en una cadena de UI, después en un payload de log, después en un breadcrumb de Sentry, y en ese punto any se ha extendido a cuatro archivos. Hacer narrowing una vez en el borde del catch mantiene honesta toda la superficie de abajo.

8. alwaysStrict

Qué atrapa. Garantiza que cada archivo parseado esté en strict mode y emite "use strict" en el JavaScript generado.

El coste. Cero en cualquier código escrito en esta década.

Por qué la dejamos puesta. Ya está activada por defecto en cualquier código con forma de módulo (ESM, módulos CommonJS, clases). El flag importa sobre todo para scripts y bloques inline. Apagarlo es un olor de que alguien está enviando scripts planos, lo que es un problema aparte.

9. strictBuiltinIteratorReturn

Qué atrapa. El value que lees del next() de un iterador nativo. TypeScript 5.6 añadió el tipo intrínseco BuiltinIteratorReturn, que es any por defecto y undefined con este flag: así const { value, done } = iter.next() sobre un MapIterator, SetIterator o ArrayIterator tipa value como posiblemente undefined hasta que compruebas done (notas de release de TypeScript 5.6).

El coste. Una comprobación de done en cualquier bucle escrito a mano sobre map.entries() o set.values(). Cero en for...of, que ya gestiona el resultado final.

Por qué la dejamos puesta. Es la misma familia de bugs que strictNullChecks, en el único sitio al que strictNullChecks no llegaba. El código que llama a next() directamente y lee value sin comprobar done recibe undefined al final de la iteración, y con el flag apagado el tipo decía any: nadie se quejaba.

Tres flags fuera de strict que también activamos

El bundle strict cubre el suelo. Tres flags están fuera y tienen un impacto real en el número de bugs en producción. Los activamos desde el día uno en cada proyecto nuevo.

noUncheckedIndexedAccess

Obliga a que obj[key] y arr[i] se tipen como T | undefined. El comportamiento por defecto asume que el acceso siempre sale bien, cosa que es falsa para cualquier record o array cuya forma sea dinámica. La doc oficial lo dice en la página de noUncheckedIndexedAccess, Sigue fuera del bundle strict, pero tsc --init ya lo escribe en una configuración nueva junto a exactOptionalPropertyTypes, así que un proyecto recién creado tiene los dos sin pedirlo (TypeScript PR 61813). Desde nuestro propio trabajo: es el flag que añade la mayor presión de type-check de cualquier ajuste individual, y elimina la clase de crash en runtime más frecuente que aún vemos en código con solo strict: true.

exactOptionalPropertyTypes

Separa "la propiedad podría no estar puesta" de "la propiedad está puesta a undefined". La mayoría de los equipos las trata como lo mismo. No lo son, sobre todo con props de React pasadas a través de wrappers y con estado de formulario donde un campo vaciado por el usuario produce una cadena vacía mientras que un campo nunca tocado produce una clave que falta. El flag impone la distinción al nivel de tipos y previene la fusión silenciosa que rompe la persistencia de los formularios.

noPropertyAccessFromIndexSignature

Obliga a obj["key"] para el acceso dinámico y a obj.key para las propiedades declaradas. La fricción es real, pero el flag previene el fall-through silencioso en el que un typo en obj.foo devuelve undefined porque el tipo tiene un index signature y el typo no coincide con ninguna propiedad declarada.

Cómo migramos un proyecto legacy a strict

La migración tiene siempre la misma forma. Se enciende strict en la raíz dentro de una rama. Se mira el número de errores. Se añade un // @ts-nocheck arriba de cada archivo que explote, se registra el recuento, y se quitan esos comentarios un archivo por pull request. No se baja strict para mantener la CI en verde; los errores que se suprimen hoy son los errores que un compañero depura a las 2 de la madrugada el mes que viene. Si el trinquete se atasca, se ejecuta tsc --noEmit -p tsconfig.strict.json solo sobre los archivos modificados, así el código nuevo paga el precio entero mientras el código viejo se sigue limpiando.

La excepción que hemos hecho, una sola vez, fue sobre strictPropertyInitialization en un proyecto NestJS donde el contenedor de DI garantiza la inicialización de los campos después de la construcción. Incluso allí, la respuesta correcta fue el operador ! sobre los campos concretos, no desactivar el flag para todo el proyecto.

El resumen en una línea

Cada flag del bundle strict previene una clase de bug que hemos visto en producción más de una vez. Tres flags fuera del bundle previenen los bugs más comunes que sobreviven a strict: true. Ninguno de los doce nos ha costado más que el bug que atrapó. Si un equipo quiere desactivar uno, la pregunta correcta es qué bug prefiere enviar.

Actualizado 2026-09-13: la familia strict son nueve flags, no ocho, desde que strictBuiltinIteratorReturn entró con TypeScript 5.6, y strict está activado por defecto desde TypeScript 6.0, marzo de 2026.

Preguntas frecuentes

Servicios relacionados

Artículos relacionados

Studio

Empieza un proyecto.

Escribimos sobre lo que construimos. Cuéntanos qué quieres construir tú.