Todo proyecto TypeScript que audito tiene "strict": true en el tsconfig.json. El mismo proyecto tiene // @ts-ignore esparcidos por el código, any en puntos críticos y nadie en el equipo capaz de explicar qué protege la flag.

"strict": true activa un grupo de flags del compilador. Cada flag existe porque una clase de bug llegó a producción antes de que alguien escribiera el test que la atrapara. El mapa de flags convierte la queja del compilador en un inventario de riesgos de tu código.

Qué habilita "strict": true

strictNullChecks

Sin ella, null y undefined entran en cualquier tipo. Con ella, el compilador exige el tratamiento de la ausencia en el punto donde el valor puede faltar.

// sin strictNullChecks: compila, rompe en runtime
function getUser(id: string): User {
  return db.find(id) // puede devolver undefined
}
const nombre = getUser('123').name // TypeError: Cannot read property 'name' of undefined

// con strictNullChecks: el compilador señala la línea
function getUser(id: string): User | undefined {
  return db.find(id)
}
const user = getUser('123')
const nombre = user?.name

La referencia de la flag describe la regla de asignabilidad. Los TypeError que sobreviven en producción y que puedo rastrear hasta el origen casi siempre son lecturas de propiedad en un valor ausente.

noImplicitAny

Sin ella, un parámetro sin tipo se vuelve any sin aviso y la verificación se apaga para la función entera. Con ella, el compilador exige el tipo declarado.

// sin noImplicitAny: el parámetro es any implícito
function process(data) {
  return data.value.toUpperCase() // no se verifica nada
}

// con noImplicitAny: el compilador pide el tipo
function process(data: { value: string }) {
  return data.value.toUpperCase()
}

La documentación de la flag es corta. En una migración es la que rompe archivos en masa: cualquier archivo sin tipos declarados falla de una vez.

strictFunctionTypes

Compara los tipos de función por la variación correcta de los parámetros. Sin la flag, el TypeScript acepta cualquier dirección; con ella, un handler que espera MouseEvent no sube a un slot que recibe Event, porque el caller puede pasar un Event puro y el handler se rompe.

type Handler = (event: Event) => void
const myHandler: Handler = (event: MouseEvent) => {
  // strictFunctionTypes señala el error
}

La declaración de método (foo() {}) sigue siendo bivariante bajo la especificación. Escribe el callback como propiedad de función cuando quieras la verificación. La referencia de la flag explica la excepción de los métodos.

strictPropertyInitialization

Exige que las propiedades de clase reciban un valor en el constructor. Evita leer un campo que el compilador trataba como listo y que llega como undefined en runtime.

class UserService {
  private db: Database // error: no recibe valor en el constructor

  constructor() {}
}

// correcto: dependencia inyectada por el constructor
class UserService {
  private db: Database

  constructor(db: Database) {
    this.db = db
  }
}

// inyección fuera del constructor, con definite assignment assertion
class UserService {
  private db!: Database
}

El operador ! registra la intención y apaga la verificación de ese campo. La documentación de la flag cubre el operador.

Dos flags fuera del strict que valen el costo

noUncheckedIndexedAccess

El acceso por índice devuelve T | undefined en lugar de T. Cubre el bug de asumir que users[0] existe, común cuando la lista viene de un filtro de API.

// tsconfig.json
{ "compilerOptions": { "noUncheckedIndexedAccess": true } }

const items = ['a', 'b', 'c']
const first = items[0] // string | undefined
const safe = first?.toUpperCase()

La flag agrega ruido en for sobre arrays y en mapas con clave dinámica. El equipo absorbe ese costo una vez y deja de recibir undefined en runtime. Documentación de la flag.

exactOptionalPropertyTypes

Separa { prop?: string } (el campo puede faltar) de { prop: string | undefined } (el campo llega, con valor undefined). Sin la flag, el TypeScript trata las dos formas como iguales y escribes un guard de null en un campo que la API envía presente. Documentación de la flag.

Migrar un proyecto legado a strict

Activar "strict": true de una sola vez en un monorepo de 200 mil líneas frena al equipo durante semanas. El camino que uso:

  1. Activa "strict": true y cubre los errores con // @ts-ignore. Acepta la deuda como temporal y visible.
  2. Instala @typescript-eslint/ban-ts-comment y configúralo para aceptar @ts-expect-error y rechazar @ts-ignore. El legado sigue en el repositorio; el código nuevo que silencia tiene que justificarse con el comentario de error esperado, que el compilador valida. @ts-ignore no valida nada.
  3. Resuelve los silenciadores archivo por archivo, empezando por los módulos con más llamadas en producción.

El código nuevo recibe strict el día uno. El legado entra según se corrige cada archivo, y el conteo de @ts-expect-error en el repositorio se convierte en el número de progreso que sigue el equipo.

El costo de silenciar al compilador

Cada // @ts-ignore, cada as any, cada flag desactivada es una apuesta: confías en tu propio razonamiento más que en la verificación de ese punto. En una librería externa con tipos malos, la apuesta se justifica. En código interno, el bug que la flag atrapaba vuelve en runtime, y el reporte del error llega semanas después, lejos de la línea que lo causó.

Cuando el compilador señala una línea y no entiendes el motivo, lee el error y corrige el tipo. El capítulo de narrowing del handbook cubre las situaciones de undefined y de tipo condicional que aparecen con más frecuencia.

Referencias