Uno de los desafíos fundamentales al integrar TypeScript con el mundo real es el límite de ejecución (Runtime Boundary). Los datos procedentes del exterior —cuerpos de peticiones HTTP, respuestas de API de terceros, mensajes de WebSockets, registros de bases de datos o parámetros de URL— ingresan en la aplicación tipados como unknown o any.
Para transformar de manera segura estructuras no verificadas en modelos de dominio estrictamente tipados, TypeScript proporciona dos mecanismos complementarios de validación y estrechamiento de tipos en el análisis de flujo de control (Control Flow Analysis, CFA):
val is Type que devuelven un valor booleano boolean, diseñadas para bifurcaciones condicionales if / else.asserts condition o asserts val is Type, las cuales lanzan una excepción si no se cumple el invariante y estrechan el tipo de la variable en el ámbito actual de manera limpia sin crear niveles innecesarios de anidamiento.+-------------------------------------------------------------------------+
| MECANISMOS DE ESTRECHAMIENTO DE DATOS NO CONFIABLES |
+-------------------------------------------------------------------------+
| [Entrada: rawPayload: unknown] |
+-------------------------------------------------------------------------+
|
+------------------------------------+
| |
v (Type Guard: Bifurcación) v (Assertion Function: Invariante)
+---------------------------------+ +------------------------------------+
| if (isUser(rawPayload)) { | | assertIsUser(rawPayload); |
| // rawPayload estrechado a | | // rawPayload estrechado a User |
| // User dentro del bloque | | // ¡directamente en el ámbito actual!
| } else { | | |
| // manejo de error | | (En caso de fallo: throw inmediato)|
| } | | |
+---------------------------------+ +------------------------------------+Al diseñar guardianes de tipos y funciones de aserción, es esencial comprender su responsabilidad arquitectónica:
Mecánica de estrechamiento mediante asserts:
function process(input: unknown) {
// input es de tipo: unknown
assertIsNonNullObject(input);
// input estrechado a: Record<string, unknown>
assertHasProperty(input, 'id');
// input estrechado a: Record<string, unknown> & { id: unknown }
assertIsString(input.id);
// input.id estrechado a: string!
console.log(input.id.toUpperCase()); // ¡Completamente seguro!
}null y undefined mediante Array.prototype.filter sin perder la precisión del compilador.process.env): Garantía de que las variables de configuración críticas existan y tengan el formato adecuado en el arranque del microservicio.useStrictContext que lanzan un error explícito si falta el proveedor de contexto en el árbol de componentes.Construyamos un conjunto de utilidades de aserción atómicas y reutilizables para estructuras de datos básicas:
/**
* Asevera la veracidad de una condición booleana arbitraria.
*/
export function assert(condition: unknown, message?: string): asserts condition {
if (!condition) {
throw new Error(message ?? '[InvariantViolation] La condición de aserción es falsa');
}
}
/**
* Asevera que un valor está definido y no es ni null ni undefined.
*/
export function assertDefined<T>(
val: T,
message?: string
): asserts val is NonNullable<T> {
if (val === null || val === undefined) {
throw new TypeError(
message ?? `[InvariantViolation] Se esperaba un valor definido, recibido: ${String(val)}`
);
}
}
/**
* Asevera que un valor es un objeto no nulo y no es un array.
*/
export function assertIsObject(
val: unknown,
message?: string
): asserts val is Record<string, unknown> {
if (typeof val !== 'object' || val === null || Array.isArray(val)) {
throw new TypeError(
message ?? `[InvariantViolation] Se esperaba un objeto no nulo, recibido: ${typeof val}`
);
}
}Combinemos predicados y funciones de aserción para validar una entidad de dominio compleja:
import { assert, assertIsObject, assertDefined } from '../utils/assertions';
export interface UserProfile {
readonly id: string;
readonly email: string;
readonly role: 'admin' | 'editor' | 'viewer';
readonly age?: number;
}
/**
* Predicado de tipo personalizado (Type Guard).
*/
export function isUserProfile(val: unknown): val is UserProfile {
try {
assertIsUserProfile(val);
return true;
} catch {
return false;
}
}
/**
* Función de aserción de contrato de dominio (Assertion Function).
*/
export function assertIsUserProfile(val: unknown): asserts val is UserProfile {
assertIsObject(val, 'El perfil de usuario debe ser un objeto no nulo');
assert(
typeof val.id === 'string' && val.id.trim().length > 0,
'El id de usuario debe ser una cadena no vacía'
);
assert(
typeof val.email === 'string' && val.email.includes('@'),
'El formato del email no es válido'
);
assert(
val.role === 'admin' || val.role === 'editor' || val.role === 'viewer',
`Rol no válido: ${String(val.role)}`
);
if ('age' in val && val.age !== undefined) {
assert(
typeof val.age === 'number' && Number.isInteger(val.age) && val.age >= 0,
'La edad age debe ser un número entero no negativo'
);
}
}
// Ejemplo de uso en el controlador:
export function handleUpdateUser(rawBody: unknown): void {
// Antes de la aserción, rawBody es de tipo unknown
assertIsUserProfile(rawBody);
// ¡Justo aquí, rawBody queda estrechado de forma SÍNCRONA a UserProfile!
console.log(`Actualizando perfil: ${rawBody.id}, rol: ${rawBody.role.toUpperCase()}`);
}La firma estándar de Array.prototype.filter(Boolean) en TypeScript devuelve el tipo de array original sin eliminar null ni undefined. Esto sucede porque el constructor global Boolean está tipado para devolver un boolean primitivo, no un predicado de tipo.
Para filtrar de manera correcta, creamos una función de predicado dedicada:
/**
* Predicado con seguridad de tipos para excluir null y undefined de colecciones.
*/
export function isNonNullable<T>(val: T): val is NonNullable<T> {
return val !== null && val !== undefined;
}
// Ejemplo de uso:
const rawList: ReadonlyArray<string | null | undefined> = ['alpha', null, 'beta', undefined];
// ❌ PROBLEMA: filter(Boolean) mantiene el tipo (string | null | undefined)[]
const badFiltered = rawList.filter(Boolean);
// ✅ SOLUCIÓN: filter(isNonNullable) devuelve un string[] limpio
const safeList: ReadonlyArray<string> = rawList.filter(isNonNullable);Recepción de datos del entorno exterior (petición HTTP, JSON.parse, process.env) con el máximo grado de incertidumbre.
Ejecución de verificaciones imperativas en JavaScript. Si se detecta un incumplimiento, la ejecución se detiene de inmediato lanzando un TypeError.
El compilador detecta la firma asserts y actualiza la tabla de símbolos para la variable dentro del ámbito léxico actual.
El código posterior accede directamente a las propiedades verificadas sin necesidad de aserciones de tipo manuales (`as`) ni comprobaciones redundantes.
import { assert, assertDefined } from '../utils/assertions';
export interface AppConfig {
readonly nodeEnv: 'development' | 'production' | 'test';
readonly port: number;
readonly databaseUrl: string;
readonly enableMetrics: boolean;
}
export function assertValidPort(val: unknown): asserts val is number {
assert(
typeof val === 'number' && Number.isInteger(val) && val > 0 && val <= 65535,
`[ConfigError] Número de puerto de red no válido: ${String(val)}`
);
}
export function assertValidDatabaseUrl(val: unknown): asserts val is string {
assert(
typeof val === 'string' &&
(val.startsWith('postgres://') || val.startsWith('postgresql://')),
`[ConfigError] DATABASE_URL debe ser una cadena de conexión válida de PostgreSQL`
);
}
/**
* Valida el entorno global de la aplicación durante el arranque en frío del microservicio.
*/
export function loadAndValidateConfig(env: Record<string, string | undefined>): AppConfig {
assertDefined(env.NODE_ENV, 'La variable de entorno NODE_ENV es obligatoria');
assert(
env.NODE_ENV === 'development' ||
env.NODE_ENV === 'production' ||
env.NODE_ENV === 'test',
`Valor no válido para NODE_ENV: ${env.NODE_ENV}`
);
const rawPort = env.PORT ? parseInt(env.PORT, 10) : 3000;
assertValidPort(rawPort);
assertDefined(env.DATABASE_URL, 'La variable DATABASE_URL debe estar definida');
assertValidDatabaseUrl(env.DATABASE_URL);
const enableMetrics = env.ENABLE_METRICS === 'true';
return {
nodeEnv: env.NODE_ENV,
port: rawPort,
databaseUrl: env.DATABASE_URL,
enableMetrics,
};
}El compilador de TypeScript confía ciegamente en el tipo de retorno declarado (val is TargetType). Si en el cuerpo de la función solo se comprueba una propiedad de diez, TypeScript considerará que todo el objeto es válido, lo que ocasionará caídas en tiempo de ejecución al acceder a los campos no verificados.
// ❌ ANTIPATRÓN: ¡Predicado con fugas!
interface Order { id: string; amount: number; items: string[]; }
function isOrderBad(val: any): val is Order {
return typeof val?.id === 'string'; // ¡items y amount NUNCA se comprueban!
}
const obj = { id: 'ord_1' };
if (isOrderBad(obj)) {
// El compilador no avisa, pero falla en ejecución: Cannot read properties of undefined (reading 'map')
obj.items.map(x => x);
}
// ✅ CORRECTO: Verificación exhaustiva de todas las propiedades obligatoriasSi se declara una función como function check(val: unknown): val is string, pero en lugar de retornar un booleano se lanza un error con throw, el compilador no podrá estrechar el tipo de la variable en las líneas posteriores fuera de un bloque if.
// ❌ ANTIPATRÓN: Sintaxis de predicado en vez de asserts
function assertBad(x: unknown): x is string {
if (typeof x !== 'string') throw new Error();
return true;
}
assertBad(data);
// ¡data SIGUE SIENDO DE TIPO unknown! ¡El predicado solo estrecha el tipo dentro de if (assertBad(data))!
// ✅ CORRECTO: Utilizar la sintaxis asserts
function assertGood(x: unknown): asserts x is string {
if (typeof x !== 'string') throw new Error();
}En las funciones flecha, TypeScript requiere indicar explícitamente : asserts condition en la declaración del tipo antes del operador =; de lo contrario, el compilador infiere void y no reconoce la firma de aserción.
// ❌ ANTIPATRÓN: El compilador infiere el retorno como void
const check = (condition: boolean) => {
if (!condition) throw new Error();
};
// ✅ CORRECTO: Declaración explícita de la firma de aserción en el tipo de la función
const checkGood: (condition: boolean) => asserts condition = (condition) => {
if (!condition) throw new Error();
};Envolver una función de aserción en un bloque try/catch vacío anula la comprobación del invariante: el flujo continúa ejecutándose sobre datos no válidos o corruptos, comprometiendo la integridad de todo el sistema.
// ❌ ANTIPATRÓN: Silenciar excepciones de aserción
try {
assertIsUser(payload);
} catch {
// ¡Bloque catch vacío!
}
// ¡payload NO está garantizado como User válido!Nunca use any para datos de entrada externos. Tipifique siempre los cuerpos y cargas de controladores como unknown.
Implemente funciones reutilizables como assertIsObject, assertDefined y assertIsString con firmas asserts.
Combine las aserciones atómicas dentro de assertIsEntity(val: unknown): asserts val is Entity para cubrir todas las propiedades.
Ejecute la función de aserción al inicio del manejador de la ruta. Todo el código posterior operará sobre el tipo de dominio validado.
Escriba pruebas negativas con null, arrays, claves ausentes y tipos erróneos para verificar que las aserciones fallen como se espera.