Cómo comprobar tipos en TypeScript (Guía completa)

Cuando empecé a crear aplicaciones empresariales con TypeScript, me encontré con un escenario confuso por el que pasa casi cualquier desarrollador: intenté escribir if (data instanceof UserInterface) o typeof data === 'UserProfile', y nada funcionó como esperaba.
¿Por qué ocurre esto? La respuesta está en la diferencia fundamental entre los tipos en tiempo de compilación y la ejecución de JavaScript en tiempo de ejecución.
En esta guía te voy a compartir todo lo que necesitas saber para comprobar tipos en TypeScript. Veremos por qué ocurre el borrado de tipos (type erasure), cómo los operadores nativos de JavaScript (typeof, instanceof, in) realizan la reducción de tipos (type narrowing), cómo aprovechar las uniones discriminadas y los predicados de tipo personalizados, y cómo usar validadores de esquemas modernos como Zod y Valibot para tener total seguridad en tiempo de ejecución.
El concepto clave: Tiempo de compilación vs. Tiempo de ejecución (Type Erasure)
Antes de escribir cualquier comprobación de tipos, tenemos que entender cómo funciona TypeScript por dentro.
TypeScript ofrece un sistema de tipos estáticos que valida tu código mientras escribes en el editor y durante el paso de compilación (tsc). Sin embargo, una vez que el compilador de TypeScript genera el código JavaScript, todos los tipos, interfaces, alias de tipo y parámetros de tipos genéricos se eliminan por completo.
// 1. Código fuente en TypeScript
interface User {
id: string;
name: string;
isAdmin: boolean;
}
function printUser(user: User) {
console.log(user.name);
}Al compilar a JavaScript, la salida es JavaScript plano sin ningún rastro de interface User:
// 2. Salida de JavaScript compilado (¡los tipos se eliminan!)
function printUser(user) {
console.log(user.name);
}Note: Como las interfaces y los alias de tipo no existen en el bundle de JavaScript compilado, no puedes inspeccionarlos en tiempo de ejecución usando operadores de JavaScript.
Para comprobar tipos cuando tu aplicación se ejecuta en el navegador o en Node.js, TypeScript se apoya en el Type Narrowing (reducción de tipos), usando comprobaciones de JavaScript en tiempo de ejecución que el compilador de TypeScript analiza de forma estática.
Ahora que entendemos por qué ocurre el borrado de tipos, pasemos a ver la primera y más sencilla comprobación en tiempo de ejecución: el operador typeof.
1. Comprobar tipos primitivos con typeof
El operador typeof de JavaScript comprueba el tipo de dato primitivo de una variable en tiempo de ejecución. El compilador de TypeScript entiende typeof dentro de bloques condicionales y reduce automáticamente el tipo de la variable.
typeof devuelve uno de los siguientes strings:
"string""number""boolean""undefined""symbol""bigint""function""object"
Ejemplo: Manejar tipos de unión
Veamos una función que da formato al identificador de una factura o a un monto numérico:
function formatIdentifier(id: string | number): string {
// En este punto, id es string | number
if (typeof id === 'number') {
// TypeScript reduce id a: number
return `INV-${id.toFixed(0).padStart(6, '0')}`;
}
// TypeScript reduce id a: string
return `INV-${id.trim().toUpperCase()}`;
}
console.log(formatIdentifier(42)); // "INV-000042"
console.log(formatIdentifier(" es-99 ")); // "INV-ES-99"Casos especiales y detalles a tener en cuenta con typeof
Aunque typeof funciona muy bien con números, strings y booleanos, tiene particularidades conocidas de JavaScript:
typeof null === 'object': En JavaScript,typeof nulldevuelve"object". Si quieres verificar que un valor sea un objeto real, comprueba siempre que no sea null:function isObject(val: unknown): boolean { return typeof val === 'object' && val !== null; }- Los arrays devuelven
"object":typeof [1, 2, 3]devuelve"object". UsaArray.isArray(val)para comprobar si se trata de un array.
Aunque typeof funciona bien para valores primitivos, en aplicaciones reales solemos trabajar con instancias de clases. Veamos cómo instanceof resuelve esto.
2. Comprobar instancias de clases con instanceof
A diferencia de las interfaces, las clases sí existen en tiempo de ejecución como funciones constructoras y prototipos. Como las clases sobreviven a la compilación, podemos usar el operador instanceof de JavaScript para comprobar si la cadena de prototipos de un objeto contiene dicho constructor.
Ejemplo real: Manejo de errores personalizados y entidades de dominio
Considera un flujo de procesamiento de facturas con clases de dominio y tipos de error personalizados:
class ApplicationError extends Error {
constructor(message: string, public readonly statusCode: number) {
super(message);
this.name = 'ApplicationError';
}
}
class NetworkError extends ApplicationError {
constructor(message: string) {
super(message, 503);
this.name = 'NetworkError';
}
}
class ValidationError extends ApplicationError {
constructor(message: string, public readonly invalidField: string) {
super(message, 400);
this.name = 'ValidationError';
}
}
function handleError(error: unknown) {
if (error instanceof ValidationError) {
// TypeScript sabe que error tiene invalidField y statusCode
console.error(`Validation failed on field [${error.invalidField}]: ${error.message}`);
} else if (error instanceof NetworkError) {
console.error(`Network issue (${error.statusCode}): ${error.message}`);
} else if (error instanceof Error) {
console.error(`Generic Error: ${error.message}`);
} else {
console.error('An unexpected non-error occurred:', error);
}
}¿Cuándo falla instanceof?
instanceof depende de las referencias a prototipos en JavaScript:
- Objetos JSON planos: Los datos recibidos de una llamada
fetch()o deJSON.parse()no tienen el prototipo de tu clase de TypeScript. Si ejecutasresponse instanceof MyClass, el resultado seráfalse. - Múltiples contextos de ejecución: Si los objetos se crean en diferentes iframes o contextos de Node.js VM, los constructores tienen referencias distintas en memoria.
Para objetos planos y datos que vienen de una API, necesitamos comprobaciones estructurales. Esto nos lleva a nuestra siguiente herramienta: el operador in.
3. Comprobar propiedades de objetos con el operador in
El operador in comprueba si una propiedad específica existe en un objeto o en su cadena de prototipos. TypeScript usa 'propiedad' in objeto para reducir tipos de unión según la existencia de esa propiedad.
Ejemplo: Diferenciar formas de objetos
Imagina que manejamos dos tipos de usuarios en nuestra aplicación:
type RegisteredUser = {
id: string;
email: string;
profileUrl: string;
};
type GuestUser = {
id: string;
sessionToken: string;
};
type AppUser = RegisteredUser | GuestUser;
function authenticateUser(user: AppUser) {
if ('email' in user) {
// TypeScript reduce automáticamente user a RegisteredUser
console.log(`Sending login confirmation to ${user.email}`);
} else {
// TypeScript reduce user a GuestUser
console.log(`Continuing with guest session: ${user.sessionToken}`);
}
}El operador in es limpio y no requiere código repetitivo. Sin embargo, cuando manejamos máquinas de estado complejas o modelos de dominio, existe un patrón todavía más claro: las uniones discriminadas.
4. Reducción con uniones discriminadas (Tagged Unions)
Las uniones discriminadas (también conocidas como tagged unions o tipos de datos algebraicos) son una de mis características favoritas en TypeScript.
Una unión discriminada se compone de múltiples tipos de objetos que comparten una propiedad literal común (el discriminante o etiqueta), como por ejemplo type, kind o status.
Ejemplo: Manejar estados asíncronos de HTTP
type IdleState = {
status: 'idle';
};
type LoadingState = {
status: 'loading';
};
type SuccessState<T> = {
status: 'success';
data: T;
receivedAt: Date;
};
type ErrorState = {
status: 'error';
error: Error;
};
type AsyncState<T> = IdleState | LoadingState | SuccessState<T> | ErrorState;Reductor de estado con seguridad de tipos y comprobación de exhaustividad
Cuando usamos una sentencia switch sobre la propiedad discriminante (status), TypeScript reduce cada rama automáticamente. También podemos agregar una comprobación de exhaustividad usando el tipo never:
function renderState<T>(state: AsyncState<T>): string {
switch (state.status) {
case 'idle':
return 'Ready to start.';
case 'loading':
return 'Fetching data, please wait...';
case 'success':
return `Data loaded successfully at ${state.receivedAt.toLocaleTimeString()}`;
case 'error':
return `Error: ${state.error.message}`;
default: {
// Comprobación de exhaustividad: si se agrega un nuevo status a AsyncState,
// ¡TypeScript mostrará un error en tiempo de compilación aquí mismo!
const _unreachable: never = state;
throw new Error(`Unhandled state: ${JSON.stringify(_unreachable)}`);
}
}
}Note: Las uniones discriminadas hacen que los estados inválidos sean imposibles de representar en tu código, eliminando categorías completas de errores en tiempo de ejecución.
Las uniones discriminadas funcionan genial cuando tú controlas la definición de los objetos. Pero ¿qué pasa cuando recibes datos sin tipar desde una fuente externa? Para eso podemos crear type guards personalizados.
5. Type guards personalizados y funciones de aserción
Cuando los operadores integrados no son suficientes, TypeScript nos permite escribir type guards personalizados usando predicados de tipo (Type Predicates).
Predicados de tipo (value is Type)
Un predicado de tipo es una función cuyo tipo de retorno tiene la forma value is TargetType. Si la función devuelve true, TypeScript reduce el argumento a TargetType en el ámbito donde se llamó.
interface InvoiceItem {
id: string;
sku: string;
price: number;
quantity: number;
}
// Type Guard personalizado
function isInvoiceItem(item: unknown): item is InvoiceItem {
return (
typeof item === 'object' &&
item !== null &&
'id' in item &&
typeof (item as Record<string, unknown>).id === 'string' &&
'sku' in item &&
typeof (item as Record<string, unknown>).sku === 'string' &&
'price' in item &&
typeof (item as Record<string, unknown>).price === 'number' &&
'quantity' in item &&
typeof (item as Record<string, unknown>).quantity === 'number'
);
}
// Uso con datos de tipo unknown
function processRawItem(rawData: unknown) {
if (isInvoiceItem(rawData)) {
// ¡TypeScript sabe que rawData es InvoiceItem!
console.log(`Processing SKU: ${rawData.sku}, Total: ${rawData.price * rawData.quantity}`);
} else {
console.warn('Invalid item format received');
}
}Funciones de aserción (asserts value is Type)
Si prefieres lanzar un error en lugar de envolver tu código dentro de comprobaciones if, puedes usar funciones de aserción:
function assertIsInvoiceItem(item: unknown): asserts item is InvoiceItem {
if (!isInvoiceItem(item)) {
throw new TypeError('Given value is not a valid InvoiceItem.');
}
}
function handleItem(item: unknown) {
// Lanza un error si no es válido; de lo contrario, la ejecución continúa
assertIsInvoiceItem(item);
// ¡En todas las líneas siguientes, item tiene el tipo InvoiceItem!
console.log(item.sku.toUpperCase());
}Escribir comprobaciones manuales para cada campo en objetos con estructuras anidadas puede ser tedioso y propenso a errores. En proyectos modernos de TypeScript, usamos librerías de validación de esquemas en tiempo de ejecución.
6. Validación moderna en tiempo de ejecución: Zod y Valibot
Para datos externos no confiables (como respuestas de APIs REST, entradas de formularios, archivos de configuración y WebSockets), el estándar moderno en el ecosistema de TypeScript es la validación basada en esquemas con Zod o Valibot.
Estas librerías validan los datos en tiempo de ejecución, eliminan campos inesperados o maliciosos, entregan mensajes de error detallados e infieren de forma automática el tipo de TypeScript para que nunca tengas que duplicar interfaces.
Validar datos con Zod
import { z } from 'zod';
// 1. Definir el esquema en tiempo de ejecución
const UserSchema = z.object({
id: z.string().uuid(),
name: z.string().min(2),
email: z.string().email(),
role: z.enum(['admin', 'member', 'guest']).default('member'),
age: z.number().int().positive().optional(),
});
// 2. Obtener automáticamente el tipo estático de TypeScript
type User = z.infer<typeof UserSchema>;
// 3. Analizar datos de entrada desconocidos de forma segura
async function fetchUserProfile(userId: string): Promise<User> {
const response = await fetch(`/api/users/${userId}`);
const rawData: unknown = await response.json();
const parseResult = UserSchema.safeParse(rawData);
if (!parseResult.success) {
console.error('Validation errors:', parseResult.error.format());
throw new Error('Invalid user payload from API');
}
// ¡parseResult.data está totalmente tipado como User!
return parseResult.data;
}Una alternativa ligera: Valibot
Si el tamaño del bundle es crítico en tu aplicación del lado del cliente, Valibot ofrece una alternativa modular y compatible con tree-shaking que funciona de manera muy similar, pero con un impacto mínimo en el bundle:
import * as v from 'valibot';
const ProductSchema = v.object({
id: v.pipe(v.string(), v.uuid()),
title: v.pipe(v.string(), v.minLength(1)),
price: v.pipe(v.number(), v.minValue(0)),
});
type Product = v.InferOutput<typeof ProductSchema>;
function parseProduct(input: unknown): Product {
return v.parse(ProductSchema, input);
}Con la validación de esquemas obtienes tanto una sólida seguridad en tiempo de ejecución como tipos de TypeScript sincronizados sin discrepancias.
Referencia rápida y matriz de decisión
Para ayudarte a elegir el enfoque adecuado para tu base de código, aquí tienes una comparación de todas las técnicas para comprobar tipos:
| Técnica | Aplica sobre | Cuándo usarla | Costo en tiempo de ejecución |
|---|---|---|---|
typeof | Primitivos (string, number, boolean, symbol, bigint, function) | Comprobaciones rápidas de primitivos y parámetros básicos de funciones | Cero |
instanceof | Clases, elementos del DOM, errores personalizados (Error, Date, RegExp) | Capturar excepciones específicas y jerarquías de clases OOP | Mínimo |
Operador in | Propiedades de objetos | Diferenciar tipos de unión mediante claves de propiedades únicas | Mínimo |
| Uniones discriminadas | Uniones de objetos con una etiqueta literal común (status, kind) | Máquinas de estado, respuestas de APIs, acciones de Redux o NgRx | Mínimo |
Type guards personalizados (is) | Estructuras de datos complejas | Validaciones internas pequeñas sin dependencias de terceros | Bajo |
| Zod / Valibot | Cualquier dato externo o desconocido | Límites de APIs, entradas de usuarios, archivos de configuración | Bajo (micro-librería) |
Conclusión y siguientes pasos
Comprobar tipos de forma efectiva en TypeScript requiere entender la frontera entre los tipos en tiempo de compilación y los valores en tiempo de ejecución:
- En tiempo de compilación: los tipos de TypeScript guían a tu editor, detectan errores a tiempo y documentan tus interfaces.
- En tiempo de ejecución: los operadores nativos (
typeof,instanceof,in), las uniones discriminadas y los type guards personalizados protegen la ejecución de tu aplicación. - En los límites externos: usa librerías de validación de esquemas como Zod o Valibot para convertir datos externos no confiables de tipo
unknownen tipos garantizados.
Combinando estas estrategias, puedes escribir aplicaciones en TypeScript resistentes y con tipos seguros que no fallen de forma inesperada en producción.
Si quieres aprender más sobre TypeScript, revisa estas guías relacionadas en mi blog:
Mastering the language of the web. From basic types to advanced generics and patterns.
Preguntas Frecuentes
¿Puedo comprobar interfaces o alias de tipo de TypeScript en tiempo de ejecución con typeof o instanceof?
No. Los tipos e interfaces de TypeScript se eliminan por completo durante el paso de compilación (un proceso conocido como type erasure o borrado de tipos). JavaScript no guarda ningún registro de las interfaces de TypeScript en tiempo de ejecución. Para validar estructuras de datos en tiempo de ejecución, necesitas type guards en tiempo de ejecución, el operador in, uniones discriminadas o librerías de validación de esquemas como Zod o Valibot.
¿Qué es el type narrowing en TypeScript?
El type narrowing (reducción de tipos) es la capacidad de TypeScript para refinar un tipo amplio (como string | number, unknown o una unión de objetos) hacia un tipo más específico dentro de un bloque condicional. TypeScript analiza sentencias de control de flujo como if (typeof x === 'string') o if ('role' in user) y reduce automáticamente el tipo de la variable dentro de esa rama.
¿Cuál es la diferencia entre un type guard personalizado y una función de aserción en TypeScript?
Un type guard personalizado usa la firma de retorno 'parameter is Type' y devuelve un booleano (true/false) para reducir el tipo dentro de una sentencia if. Una función de aserción usa la sintaxis 'asserts condition' o 'asserts parameter is Type' y lanza un error si la comprobación falla, reduciendo el tipo de la variable para el resto del ámbito de ejecución actual sin anidar código en bloques if.
¿Cuándo debería usar librerías de validación de esquemas como Zod o Valibot en lugar de type guards manuales?
Los type guards manuales son rápidos y no requieren dependencias para comprobaciones simples o internas. Sin embargo, al trabajar con límites externos no confiables (como respuestas de APIs HTTP, entradas de formularios de usuarios, payloads de webhooks o localStorage), librerías como Zod y Valibot ofrecen validación sólida en tiempo de ejecución, análisis de estructuras anidadas, mensajes de error claros y deducción automática de tipos estáticos con z.infer.
Artículos Relacionados
Por qué deberías evitar usar 'any' en TypeScript
Por qué usar any en TypeScript genera errores ocultos en tiempo de ejecución, y cómo reemplazarlo con unknown, guardas de tipo personalizadas, tipos de utilidad y el operador satisfies.
3 formas de transformar tipos en TypeScript
Cuando usamos tipos o interfaces en TypeScript, el compilador exige que el objeto coincida con ellos para evitar errores en tiempo de ejecución por propiedades faltantes. A veces queremos la flexibilidad de crear un objeto sin romper el contrato con la interfaz.
Cómo combinar tipos e interfaces con & y | en TypeScript
Aprende a componer tipos en TypeScript usando Union (|), Intersection (&) e interface extends con ejemplos prácticos, tablas de decisión y patrones reales.
Interfaces en TypeScript con un ejemplo
Las interfaces son una excelente funcionalidad de TypeScript para escribir código estructurado y explícito. Te permiten describir contratos con propiedades y métodos que tus objetos y clases deben cumplir.
Compartir este artículo
Si esta guía te resultó útil, compártela con tu equipo o con otros desarrolladores.
Software real. Lecciones reales.
Comparto las lecciones que aprendí por las malas, para que puedas evitarlas o estar preparado cuando ocurran.
Únete a 13,800+ desarrolladores y lectores.
Cero spam. Cancela tu suscripción en cualquier momento.