TypeScript strict mode: los 8 ajustes que nunca apagamos
El flag strict: true de TypeScript activa 8 comprobaciones separadas. La mayoría de los proyectos deja strict puesto en la raíz y desactiva una o dos en silencio dentro de un archivo problemático. Aquí están los 8, qué atrapan en producción y los tres flags fuera del bundle que activamos igualmente en cada proyecto.
TypeScript strict mode es un único flag dentro de tsconfig.json que activa ocho comprobaciones separadas del compilador a la vez. Las ocho son strictNullChecks, noImplicitAny, strictFunctionTypes, strictBindCallApply, strictPropertyInitialization, noImplicitThis, useUnknownInCatchVariables y alwaysStrict. La referencia TSConfig de Microsoft las lista en la familia strict, y el equipo de Microsoft tiene una propuesta abierta (issue 62333) para hacer que strict sea el valor por defecto en una futura versión mayor.
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 ocho, 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 ocho, 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.
Las 8 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.
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, y la issue 49169 del repo de TypeScript es una discusión de largo recorrido sobre si debería entrar en strict por defecto. 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 once nos ha costado más que el bug que atrapó. Si un equipo quiere desactivar uno, la pregunta correcta es qué bug prefiere enviar.
Preguntas frecuentes
- What are the 8 flags that TypeScript strict mode enables?
- The strict family contains eight flags: strictNullChecks, noImplicitAny, strictFunctionTypes, strictBindCallApply, strictPropertyInitialization, noImplicitThis, useUnknownInCatchVariables, and alwaysStrict. Setting strict: true in tsconfig.json turns on all eight at once. Each one targets a specific class of bug, so disabling any single flag while leaving strict: true at the root creates a hidden hole the rest of the codebase no longer guards against.
- Should noUncheckedIndexedAccess be enabled by default in TypeScript?
- We think yes for any new project, even though it is not inside the strict bundle. The default behaviour types obj[key] and arr[i] as if the access always succeeds, which is wrong for any record or array with a dynamic shape. Issue 49169 in the TypeScript repository tracks the long-running discussion about folding it into strict. The cost is real: it produces the most new type errors of any single flag in our experience. The benefit is that it removes the most common runtime crash class that still survives in code with strict: true alone, particularly around API responses, Record lookups, and array indexing.
- Can we enable TypeScript strict mode incrementally on an existing project?
- Yes, and the order matters. Turn on strict at the root in a branch, then add // @ts-nocheck at the top of any file that produces too many errors to fix in one pass. Strip those comments one file per pull request, prioritising files closest to the user (API handlers, form code, payment flows). Inside the strict bundle, leave strictNullChecks for last: it produces the largest single error count and the highest cleanup effort. Do not lower strict to make CI green. Suppression at the file level is reversible; suppression at the config level rarely is.
- Does TypeScript strict mode slow down the compiler?
- Not in any way that matters. The strict flags change how types are checked, not how much work the type-checker does on each file. Build time and tsc --noEmit time are essentially identical with strict on or off. The real cost is in developer time spent fixing the errors strict surfaces, which is a one-time cost on a clean migration and an ongoing tax on new code that pays itself back the first time the flag prevents a production bug.
Studio
Empieza un proyecto.
Un partner único para el producto digital que necesitas construir. Producción más rápida, tecnología moderna, costes reducidos. Un equipo, una factura.