Strict mode en TypeScript: qué se activa
1. Descripción general
"strict": true es la línea más importante en tsconfig.json. Pero muchas personas la activan sin saber qué realmente están habilitando. Este artículo recorre cada flag que strict activa, los errores reales que cada uno atrapa, y algunos flags que deberías activar además. Al final encontrarás cómo migrar un proyecto antiguo a strict sin tener que corregir miles de errores de una vez.
2. strict es un grupo de flags
strict no es una comprobación individual. Es un interruptor general que activa varios flags a la vez:
{
"compilerOptions": {
"target": "ES2022",
"module": "NodeNext",
"strict": true
}
}
Con TypeScript 5.x y 6.x, strict: true es equivalente a:
{
"compilerOptions": {
"noImplicitAny": true,
"strictNullChecks": true,
"strictFunctionTypes": true,
"strictBindCallApply": true,
"strictPropertyInitialization": true,
"strictBuiltinIteratorReturn": true,
"noImplicitThis": true,
"useUnknownInCatchVariables": true,
"alwaysStrict": true
}
}
Hay dos cosas que necesitas recordar:
- Cada vez que TypeScript añade una nueva comprobación “strict”, por ejemplo
useUnknownInCatchVariablesen 4.4 ostrictBuiltinIteratorReturnen 5.6, esa comprobación se activa automáticamente constrict. Por lo tanto, actualizar TypeScript puede hacer aparecer nuevos errores incluso si no tocas el código. - Puedes activar
stricty luego desactivar un flag específico. Los flags explícitos siempre tienen precedencia sobre el flag general.
TypeScript 6.0 ha cambiado el valor por defecto de strict a true. De todas formas, aún deberías especificar este flag en tu tsconfig.json, para que quien lea la configuración no tenga que adivinarlo.
Para probar los ejemplos de abajo, solo necesitas instalar TypeScript y ejecutar tsc --noEmit:
npm install --save-dev typescript
npx tsc --noEmit
3. Los flags más importantes
3.1. noImplicitAny
Cuando TypeScript no puede inferir un tipo, asigna implícitamente any. Este flag convierte ese caso en un error.
function calculateTotal(items) {
return items.reduce((sum, item) => sum + item.price, 0);
}
error TS7006: Parameter 'items' implicitly has an 'any' type.
Sin este flag, items sería any. Cuando es any, escribir item.prise (error de tipografía) compilaría normalmente. La forma de corregirlo es declarar explícitamente el tipo:
interface OrderItem {
name: string;
price: number;
}
function calculateTotal(items: OrderItem[]): number {
return items.reduce((sum, item) => sum + item.price, 0);
}
Este flag solo bloquea any implícito. Aún puedes escribir any explícitamente si realmente lo necesitas.
3.2. strictNullChecks
Este es el flag que más vale la pena. Cuando está desactivado, null y undefined se pueden asignar a cualquier tipo. Cuando está activado, se convierten en tipos propios y debes manejarlos explícitamente.
interface Customer {
id: number;
email: string;
}
const customers: Customer[] = [{ id: 1, email: "an@example.com" }];
function getCustomerEmail(id: number): string {
const customer = customers.find((c) => c.id === id);
return customer.email;
}
error TS18048: 'customer' is possibly 'undefined'.
Array.prototype.find devuelve Customer | undefined. Sin este flag, TypeScript ignora undefined, y el error Cannot read properties of undefined solo aparece en runtime. La forma de corregirlo:
function getCustomerEmail(id: number): string | undefined {
const customer = customers.find((c) => c.id === id);
return customer?.email;
}
Ahora la función devuelve string | undefined. El tipo de retorno dice la verdad, por lo que donde llames la función también debes manejar el caso en que no se encuentre.
Nota: No “corrijas” el error usando el operador non-null
customer!.email. El operador!solo hace que el compilador calle, no verifica nada. Solo úsalo cuando estés seguro de que el valor existe y tengas una razón por la que el compilador no pueda verlo.
3.3. strictPropertyInitialization
Este flag requiere que toda propiedad de clase sea asignada a un valor, ya sea en la declaración o en el constructor. Necesita strictNullChecks activado para funcionar.
class PaymentService {
apiKey: string;
retries = 3;
constructor(private readonly baseUrl: string) {}
}
error TS2564: Property 'apiKey' has no initializer and is not definitely assigned in the constructor.
Tienes tres maneras de manejarlo:
class PaymentService {
apiKey: string; // 1. asignar en el constructor
webhookSecret?: string; // 2. declarar como opcional
client!: HttpClient; // 3. asignación definitiva: "confía en mí, se asignará"
constructor(apiKey: string) {
this.apiKey = apiKey;
}
}
interface HttpClient {
post(url: string, body: unknown): Promise<unknown>;
}
La opción 3 (!) es común con frameworks que usan inyección de dependencias o decoradores, como Angular o TypeORM. Allí la propiedad se asigna después de que el constructor termina. Fuera de esos casos, prefiere la opción 1 o 2.
3.4. useUnknownInCatchVariables
En JavaScript, puedes hacer throw de cualquier cosa, no necesariamente un Error. Este flag cambia el tipo de la variable en catch de any a unknown.
async function chargeOrder(orderId: string): Promise<void> {
try {
await fetch(`/api/orders/${orderId}/charge`, { method: "POST" });
} catch (err) {
console.error(err.message);
}
}
error TS18046: 'err' is of type 'unknown'.
Para usar err, primero tienes que estrechar el tipo:
catch (err) {
const message = err instanceof Error ? err.message : String(err);
console.error(`Cargo fallido: ${message}`);
}
3.5. strictFunctionTypes
Este flag verifica los parámetros de un tipo función usando varianza contravariante. En pocas palabras: no puedes pasar una función que solo maneja un tipo más específico donde se espera una función que maneja un tipo más general.
interface Animal { name: string; }
interface Dog extends Animal { bark(): void; }
const handleDog = (dog: Dog) => dog.bark();
const handleAnimal: (animal: Animal) => void = handleDog;
error TS2322: Type '(dog: Dog) => void' is not assignable to type '(animal: Animal) => void'.
Si la asignación anterior fuera aceptada, llamar handleAnimal({ name: "Gato" }) causaría un error porque el objeto no tiene bark. Hay una excepción importante que debes conocer: este flag no se aplica a métodos declarados con la sintaxis method(x: T): void en una interface. Esto es una decisión intencional de TypeScript para mantener compatibilidad con muchas APIs existentes.
3.6. Los tres flags restantes
Estos tres flags rara vez causan errores en código nuevo, pero vale la pena conocerlos:
function formatPrice(amount: number, currency: string): string {
return `${amount.toFixed(2)} ${currency}`;
}
formatPrice.call(null, "100", "VND"); // strictBindCallApply
function onClick() {
console.log(this.id); // noImplicitThis
}
error TS2345: Argument of type 'string' is not assignable to parameter of type 'number'.
error TS2683: 'this' implicitly has type 'any' because it does not have a type annotation.
strictBindCallApplyverifica los parámetros cuando llamascall,applyybind.noImplicitThislanza un error cuandothistiene tipoanyimplícitamente. Lo corriges declarando un parámetro ficticiofunction onClick(this: HTMLButtonElement).alwaysStrictemite"use strict"en cada archivo de salida y analiza el código en strict mode de JavaScript. Con módulos ES, strict mode es ya el defecto.
4. Flags que strict no activa
Mucha gente cree que strict: true es el máximo nivel de severidad. No es cierto. Los siguientes flags están fuera del grupo strict, pero son muy recomendables para proyectos nuevos:
{
"compilerOptions": {
"strict": true,
"noUncheckedIndexedAccess": true,
"exactOptionalPropertyTypes": true,
"noImplicitOverride": true,
"noImplicitReturns": true,
"noFallthroughCasesInSwitch": true
}
}
Entre estos, noUncheckedIndexedAccess es el más importante. Por defecto, TypeScript considera que acceder a un array por índice o a un objeto por clave dinámica siempre tiene un valor:
const stockBySku: Record<string, number> = { "SKU-1": 10 };
const quantity = stockBySku["SKU-999"];
console.log(quantity.toFixed(0));
Sin este flag, quantity tiene tipo number y el código anterior fallaría en runtime. Cuando activas el flag, quantity tiene tipo number | undefined:
error TS18048: 'quantity' is possibly 'undefined'.
Los otros flags:
exactOptionalPropertyTypes:discount?: numbersignifica “la clave puede no estar presente”, no permite asignar explícitamentediscount: undefined.noImplicitOverride: un método que sobrescribe la clase padre debe tener la palabra claveoverride. Así, si la clase padre cambia el nombre del método, la clase hija lanzará un error en lugar de crear silenciosamente un nuevo método.noImplicitReturns: toda rama de la función debe hacerreturn.noFallthroughCasesInSwitch: previene olvidarbreaken unswitch.
Nota:
noUncheckedIndexedAccesses bastante “ruidoso”. Incluso un buclefor (let i = 0; i < arr.length; i++)te daráarr[i]con tipoT | undefined. Cambia afor...ofoarr.forEach. Para proyectos grandes existentes, activa este flag al final.
5. Verifica el comportamiento con tests
Type check se ejecuta en tiempo de compilación, pero aún debes tener tests para tanto el comportamiento en runtime como los tipos de retorno. Vitest soporta ambos:
npm install --save-dev vitest
// customer.ts
export interface Customer { id: number; email: string; }
const customers: Customer[] = [{ id: 1, email: "an@example.com" }];
export function getCustomerEmail(id: number): string | undefined {
return customers.find((c) => c.id === id)?.email;
}
// customer.test.ts
import { describe, it, expect, expectTypeOf } from "vitest";
import { getCustomerEmail } from "./customer";
describe("getCustomerEmail", () => {
it("devuelve email cuando encuentra customer", () => {
expect(getCustomerEmail(1)).toBe("an@example.com");
});
it("devuelve undefined cuando no encuentra", () => {
expect(getCustomerEmail(42)).toBeUndefined();
});
it("el tipo de retorno incluye undefined", () => {
expectTypeOf(getCustomerEmail).returns.toEqualTypeOf<string | undefined>();
});
});
El tercer test es un type test. Supongamos que después alguien cambia la firma de la función a : string y usa ! para forzar el tipo. Entonces vitest --typecheck (o tsc --noEmit en el archivo test) lanzará un error, porque el tipo de retorno no coincide con string | undefined.
npx vitest run --typecheck
✓ customer.test.ts (3 tests)
Test Files 1 passed (1)
Tests 3 passed (3)
6. Migrar un proyecto antiguo a strict
Activar strict: true en un codebase grande puede generar miles de errores. La forma práctica es hacerlo paso a paso.
Paso 1: activa strict, pero desactiva temporalmente los flags más ruidosos.
{
"compilerOptions": {
"strict": true,
"strictNullChecks": false,
"strictPropertyInitialization": false,
"useUnknownInCatchVariables": false
}
}
noImplicitAny es generalmente el más fácil de corregir, así que comienza por ahí. Cada vez que termines de arreglar un flag, elimina la línea que lo desactiva. El código nuevo que escribas automáticamente seguirá todas las comprobaciones activas.
Paso 2: cuenta los errores por flag para estimar el esfuerzo.
npx tsc --noEmit --strictNullChecks true | grep -c "error TS"
Paso 3: con strictNullChecks, corrige por carpeta. Marca temporalmente los lugares que no hayas tenido tiempo de corregir con // @ts-expect-error, incluida una nota sobre por qué. No uses @ts-ignore. Razón: @ts-expect-error lanzará un error automáticamente cuando la línea debajo ya no tenga error, así que no se te olvidará eliminar el comentario.
// @ts-expect-error TODO(strict): customer puede ser undefined, manejar en PR #482
const email: string = findCustomer(id).email;
Paso 4: bloquea regresión en CI. Añade tsc --noEmit a tu pipeline para que nadie pueda hacer merge de código que introduzca nuevos errores de tipo.
Nota: No uses
skipLibCheckoanyen todas partes como forma de “activar strict rápidamente”.skipLibCheck: truesolo omite la verificación de archivos.d.ts, así que es seguro mantenerlo. Pero esparciranypor todas partes te hará perder todos los beneficios de la migración.
7. Conclusión
strict: true activa nueve comprobaciones. Las dos que más valor aportan son noImplicitAny y strictNullChecks, porque atrapan la mayoría de errores undefined is not a function y Cannot read properties of undefined en tiempo de compilación. Para proyectos nuevos, deberías activar strict junto con noUncheckedIndexedAccess, noImplicitOverride y exactOptionalPropertyTypes desde el inicio. Para proyectos antiguos, activa strict y desactiva temporalmente cada flag, corrige gradualmente en orden de fácil a difícil, y usa CI para asegurar que el número de errores solo disminuya, nunca aumente.