Type guards con typeof e instanceof
Los type guards son expresiones que realizan una comprobación en tiempo de ejecución para garantizar el tipo de una variable dentro de un bloque específico. TypeScript utiliza esta información para refinar automáticamente el tipo de la variable, proporcionando mejor autocompletado y detección de errores.
Cuando trabajamos con tipos de unión, necesitamos una forma segura de determinar qué tipo específico estamos manejando en cada momento. Los type guards nos permiten estrechar el tipo (narrowing) de manera que TypeScript entienda exactamente con qué estamos trabajando.
Operador typeof para tipos primitivos
El operador typeof
es el type guard más básico y se utiliza principalmente para distinguir entre tipos primitivos como string
, number
, boolean
y otros tipos básicos de JavaScript.
function procesarDato(valor: string | number): string {
if (typeof valor === "string") {
// Aquí TypeScript sabe que valor es string
return valor.toUpperCase();
} else {
// Aquí TypeScript sabe que valor es number
return valor.toFixed(2);
}
}
const texto = procesarDato("hola mundo"); // "HOLA MUNDO"
const numero = procesarDato(42.7589); // "42.76"
El análisis de control de flujo de TypeScript reconoce automáticamente que después de la comprobación typeof valor === "string"
, la variable valor
es definitivamente de tipo string
dentro de ese bloque.
Casos prácticos con typeof
Los type guards con typeof
son especialmente útiles para validar entrada de datos y manejar funciones que pueden recibir múltiples tipos:
function formatearSalida(data: string | number | boolean): string {
if (typeof data === "string") {
return `Texto: "${data}"`;
}
if (typeof data === "number") {
return `Número: ${data.toLocaleString()}`;
}
if (typeof data === "boolean") {
return `Booleano: ${data ? "Verdadero" : "Falso"}`;
}
// TypeScript detecta que este punto nunca debería alcanzarse
return "Tipo no soportado";
}
Operador instanceof para clases y objetos
El operador instanceof
permite verificar si un objeto es una instancia de una clase específica o si fue creado por un constructor particular. Es especialmente útil cuando trabajamos con jerarquías de clases.
class Usuario {
constructor(public nombre: string) {}
saludar(): string {
return `Hola, soy ${this.nombre}`;
}
}
class Administrador extends Usuario {
constructor(nombre: string, public permisos: string[]) {
super(nombre);
}
gestionarSistema(): string {
return `${this.nombre} está gestionando el sistema`;
}
}
function procesarPersona(persona: Usuario | Administrador): string {
if (persona instanceof Administrador) {
// Aquí TypeScript sabe que persona es Administrador
return persona.gestionarSistema();
}
// Aquí TypeScript sabe que persona es Usuario
return persona.saludar();
}
Trabajando con objetos del DOM
El operador instanceof
es particularmente útil cuando trabajamos con elementos del DOM, donde diferentes tipos de elementos tienen métodos y propiedades específicas:
function manejarElemento(elemento: HTMLElement): void {
if (elemento instanceof HTMLInputElement) {
// Acceso seguro a propiedades específicas de input
console.log(`Valor del input: ${elemento.value}`);
elemento.focus();
} else if (elemento instanceof HTMLButtonElement) {
// Acceso seguro a propiedades específicas de button
console.log(`Texto del botón: ${elemento.textContent}`);
elemento.disabled = true;
} else if (elemento instanceof HTMLImageElement) {
// Acceso seguro a propiedades específicas de img
console.log(`URL de la imagen: ${elemento.src}`);
elemento.alt = "Imagen cargada";
}
}
Combinar typeof e instanceof
En aplicaciones reales, frecuentemente combinamos ambos operadores para crear validaciones más robustas:
type DatoComplejo = string | number | Date | Usuario;
function analizarDato(dato: DatoComplejo): string {
// Primero verificamos tipos primitivos
if (typeof dato === "string") {
return `Es una cadena de ${dato.length} caracteres`;
}
if (typeof dato === "number") {
return `Es un número: ${dato}`;
}
// Luego verificamos instancias de clases
if (dato instanceof Date) {
return `Es una fecha: ${dato.toLocaleDateString()}`;
}
if (dato instanceof Usuario) {
return `Es un usuario: ${dato.nombre}`;
}
// Nunca debería llegar aquí
return "Tipo desconocido";
}
Limitaciones importantes
Es crucial entender que typeof
tiene limitaciones específicas. Para arrays y objetos, typeof
siempre retorna "object"
, por lo que no es útil para distinguir entre estos tipos:
// typeof no es efectivo aquí
function procesarEstructura(data: string[] | { nombre: string }): void {
if (typeof data === "object") {
// Esto no ayuda - tanto arrays como objetos son "object"
// Necesitaríamos usar Array.isArray() o el operador 'in'
}
}
Los type guards con typeof
e instanceof
forman la base fundamental para el manejo seguro de tipos en TypeScript, proporcionando una forma natural y eficiente de trabajar con tipos de unión mientras mantenemos la seguridad de tipos en tiempo de compilación.
Type guards personalizados (user-defined)
Los type guards personalizados nos permiten crear nuestra propia lógica de verificación de tipos cuando los operadores nativos como typeof
e instanceof
no son suficientes. Estos guards utilizan predicados de tipo (type predicates) con la sintaxis valor is TipoEspecífico
para informar a TypeScript sobre el tipo real de una variable.
Un type guard personalizado es una función que retorna un booleano y utiliza la sintaxis especial parameter is Type
en su declaración de retorno. Cuando esta función retorna true
, TypeScript automáticamente refina el tipo de la variable en el ámbito correspondiente.
Sintaxis básica de type guards personalizados
La estructura fundamental de un type guard personalizado sigue este patrón:
function esString(valor: unknown): valor is string {
return typeof valor === "string";
}
function procesarValor(input: unknown): string {
if (esString(input)) {
// Aquí TypeScript sabe que input es string
return input.toUpperCase();
}
return "No es un string";
}
El predicado de tipo valor is string
le dice a TypeScript que si la función retorna true
, el parámetro valor
debe ser tratado como string
en el código subsecuente.
Type guards para interfaces y objetos complejos
Los type guards personalizados son especialmente valiosos para verificar la estructura de objetos y determinar si un valor cumple con una interfaz específica:
interface Producto {
id: number;
nombre: string;
precio: number;
}
interface Cliente {
id: number;
nombre: string;
email: string;
}
function esProducto(objeto: unknown): objeto is Producto {
return (
typeof objeto === "object" &&
objeto !== null &&
typeof (objeto as Producto).id === "number" &&
typeof (objeto as Producto).nombre === "string" &&
typeof (objeto as Producto).precio === "number"
);
}
function esCliente(objeto: unknown): objeto is Cliente {
return (
typeof objeto === "object" &&
objeto !== null &&
typeof (objeto as Cliente).id === "number" &&
typeof (objeto as Cliente).nombre === "string" &&
typeof (objeto as Cliente).email === "string"
);
}
function procesarDatos(data: unknown): string {
if (esProducto(data)) {
// TypeScript conoce todas las propiedades de Producto
return `Producto: ${data.nombre} - €${data.precio}`;
}
if (esCliente(data)) {
// TypeScript conoce todas las propiedades de Cliente
return `Cliente: ${data.nombre} (${data.email})`;
}
return "Datos no válidos";
}
Validación robusta con múltiples condiciones
Los type guards personalizados permiten implementar validaciones complejas que van más allá de simples comprobaciones de tipo:
interface UsuarioAutenticado {
id: number;
username: string;
token: string;
permisos: string[];
}
function esUsuarioAutenticado(usuario: unknown): usuario is UsuarioAutenticado {
if (typeof usuario !== "object" || usuario === null) {
return false;
}
const u = usuario as UsuarioAutenticado;
return (
typeof u.id === "number" &&
u.id > 0 &&
typeof u.username === "string" &&
u.username.length > 0 &&
typeof u.token === "string" &&
u.token.length > 0 &&
Array.isArray(u.permisos) &&
u.permisos.every(permiso => typeof permiso === "string")
);
}
function accederRecursoProtegido(usuario: unknown): string {
if (esUsuarioAutenticado(usuario)) {
// Acceso seguro a todas las propiedades
return `Bienvenido ${usuario.username}, tienes ${usuario.permisos.length} permisos`;
}
return "Acceso denegado: usuario no autenticado";
}
Type guards para tipos de unión discriminados
Cuando trabajamos con tipos de unión complejos, los type guards personalizados nos ayudan a distinguir entre diferentes variantes:
interface EventoClick {
tipo: "click";
elemento: string;
coordenadas: { x: number; y: number };
}
interface EventoTeclado {
tipo: "keyboard";
tecla: string;
modificadores: string[];
}
interface EventoScroll {
tipo: "scroll";
direccion: "up" | "down";
velocidad: number;
}
type Evento = EventoClick | EventoTeclado | EventoScroll;
function esEventoClick(evento: Evento): evento is EventoClick {
return evento.tipo === "click";
}
function esEventoTeclado(evento: Evento): evento is EventoTeclado {
return evento.tipo === "keyboard";
}
function esEventoScroll(evento: Evento): evento is EventoScroll {
return evento.tipo === "scroll";
}
function procesarEvento(evento: Evento): string {
if (esEventoClick(evento)) {
return `Click en ${evento.elemento} en (${evento.coordenadas.x}, ${evento.coordenadas.y})`;
}
if (esEventoTeclado(evento)) {
return `Tecla presionada: ${evento.tecla} con modificadores: ${evento.modificadores.join(", ")}`;
}
if (esEventoScroll(evento)) {
return `Scroll ${evento.direccion} a velocidad ${evento.velocidad}`;
}
return "Evento desconocido";
}
Combinar type guards personalizados
Los type guards se pueden componer y combinar para crear validaciones más sofisticadas:
function esNumeroPositivo(valor: unknown): valor is number {
return typeof valor === "number" && valor > 0;
}
function esArrayDeNumeros(valor: unknown): valor is number[] {
return Array.isArray(valor) && valor.every(item => typeof item === "number");
}
function esDatosValidosParaCalculo(datos: unknown): datos is { valores: number[]; multiplicador: number } {
if (typeof datos !== "object" || datos === null) {
return false;
}
const d = datos as { valores: unknown; multiplicador: unknown };
return (
esArrayDeNumeros(d.valores) &&
esNumeroPositivo(d.multiplicador)
);
}
function calcularResultado(input: unknown): number {
if (esDatosValidosParaCalculo(input)) {
// TypeScript garantiza la estructura y tipos correctos
return input.valores.reduce((sum, val) => sum + val, 0) * input.multiplicador;
}
throw new Error("Datos inválidos para el cálculo");
}
Type guards con aserciones de tipo
Los type guards personalizados también pueden utilizarse junto con funciones de aserción que lanzan errores en lugar de retornar booleanos:
function assertEsEmail(valor: unknown): asserts valor is string {
if (typeof valor !== "string") {
throw new Error("El valor debe ser una cadena");
}
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
if (!emailRegex.test(valor)) {
throw new Error("Formato de email inválido");
}
}
function procesarEmail(input: unknown): string {
assertEsEmail(input);
// Después de la aserción, TypeScript sabe que input es string
return `Email procesado: ${input.toLowerCase()}`;
}
Los type guards personalizados proporcionan flexibilidad total para crear validaciones de tipos adaptadas a las necesidades específicas de nuestra aplicación, manteniendo la seguridad de tipos que caracteriza a TypeScript mientras ofrecemos una API clara y expresiva para el manejo de datos complejos.
Type guards con operador in
El operador in
es un type guard nativo de JavaScript que TypeScript aprovecha para realizar verificaciones de tipo basadas en la presencia de propiedades específicas en un objeto. Este operador es especialmente efectivo para distinguir entre tipos de unión cuando cada tipo tiene propiedades únicas que lo identifican.
A diferencia de typeof
e instanceof
, el operador in
nos permite verificar si una propiedad específica existe en un objeto, lo que resulta ideal para trabajar con interfaces y tipos de unión discriminados donde diferentes tipos tienen estructuras distintas.
Sintaxis y funcionamiento básico
La sintaxis del operador in
es simple: 'propiedad' in objeto
. Retorna true
si la propiedad existe en el objeto (incluso si su valor es undefined
) y false
en caso contrario:
interface Coche {
marca: string;
ruedas: number;
acelerar(): void;
}
interface Barco {
nombre: string;
eslora: number;
navegar(): void;
}
function moverVehiculo(vehiculo: Coche | Barco): string {
if ('acelerar' in vehiculo) {
// TypeScript sabe que vehiculo es Coche
vehiculo.acelerar();
return `El coche ${vehiculo.marca} está acelerando`;
} else {
// TypeScript sabe que vehiculo es Barco
vehiculo.navegar();
return `El barco ${vehiculo.nombre} está navegando`;
}
}
El análisis de flujo de control de TypeScript reconoce que después de verificar 'acelerar' in vehiculo
, la variable debe ser de tipo Coche
, ya que es el único tipo que tiene esa propiedad.
Type guards para propiedades opcionales
El operador in
es particularmente útil para distinguir entre objetos que tienen propiedades opcionales diferentes:
interface UsuarioBasico {
id: number;
nombre: string;
}
interface UsuarioPremium {
id: number;
nombre: string;
beneficiosPremium?: string[];
descuentoMaximo?: number;
}
interface UsuarioEmpresarial {
id: number;
nombre: string;
empresa?: string;
limitesEspeciales?: { [key: string]: number };
}
type Usuario = UsuarioBasico | UsuarioPremium | UsuarioEmpresarial;
function obtenerPrivilegios(usuario: Usuario): string {
if ('beneficiosPremium' in usuario) {
// TypeScript identifica como UsuarioPremium
const beneficios = usuario.beneficiosPremium?.length || 0;
return `Usuario Premium con ${beneficios} beneficios`;
}
if ('empresa' in usuario) {
// TypeScript identifica como UsuarioEmpresarial
return `Usuario empresarial de ${usuario.empresa || 'empresa no especificada'}`;
}
// TypeScript identifica como UsuarioBasico
return `Usuario básico: ${usuario.nombre}`;
}
Verificación de múltiples propiedades
Podemos combinar múltiples verificaciones con el operador in
para crear type guards más precisos:
interface ConfiguracionBasica {
tema: string;
idioma: string;
}
interface ConfiguracionAvanzada extends ConfiguracionBasica {
notificaciones: boolean;
modoDesarrollador: boolean;
}
interface ConfiguracionEmpresarial extends ConfiguracionAvanzada {
politicasSeguridad: string[];
auditoria: boolean;
}
type Configuracion = ConfiguracionBasica | ConfiguracionAvanzada | ConfiguracionEmpresarial;
function aplicarConfiguracion(config: Configuracion): string {
if ('politicasSeguridad' in config && 'auditoria' in config) {
// TypeScript identifica como ConfiguracionEmpresarial
return `Configuración empresarial: ${config.politicasSeguridad.length} políticas de seguridad`;
}
if ('notificaciones' in config && 'modoDesarrollador' in config) {
// TypeScript identifica como ConfiguracionAvanzada
const modo = config.modoDesarrollador ? 'desarrollador' : 'producción';
return `Configuración avanzada en modo ${modo}`;
}
// TypeScript identifica como ConfiguracionBasica
return `Configuración básica: tema ${config.tema}, idioma ${config.idioma}`;
}
Type guards con propiedades anidadas
El operador in
también funciona efectivamente para verificar propiedades anidadas en estructuras de objetos complejas:
interface EventoLocal {
tipo: 'local';
ubicacion: {
direccion: string;
ciudad: string;
codigoPostal: string;
};
}
interface EventoOnline {
tipo: 'online';
plataforma: {
url: string;
credenciales?: { usuario: string; password: string };
};
}
interface EventoHibrido {
tipo: 'hibrido';
ubicacion: {
direccion: string;
ciudad: string;
};
plataforma: {
url: string;
};
}
type Evento = EventoLocal | EventoOnline | EventoHibrido;
function prepararEvento(evento: Evento): string {
if ('ubicacion' in evento && 'codigoPostal' in evento.ubicacion) {
// TypeScript identifica como EventoLocal
return `Evento presencial en ${evento.ubicacion.direccion}, ${evento.ubicacion.ciudad}`;
}
if ('plataforma' in evento && 'ubicacion' in evento) {
// TypeScript identifica como EventoHibrido
return `Evento híbrido: ${evento.ubicacion.direccion} + ${evento.plataforma.url}`;
}
if ('plataforma' in evento) {
// TypeScript identifica como EventoOnline
const acceso = evento.plataforma.credenciales ? 'con credenciales' : 'acceso libre';
return `Evento online en ${evento.plataforma.url} (${acceso})`;
}
return "Tipo de evento no reconocido";
}
Comparación con otras técnicas
El operador in
complementa perfectamente otros type guards y técnicas de narrowing. Es especialmente útil cuando no podemos usar propiedades discriminantes simples:
interface Notificacion {
id: string;
mensaje: string;
timestamp: Date;
}
interface NotificacionEmail extends Notificacion {
destinatario: string;
asunto: string;
}
interface NotificacionPush extends Notificacion {
dispositivos: string[];
badge?: number;
}
interface NotificacionSMS extends Notificacion {
telefono: string;
longitud: number;
}
type TipoNotificacion = NotificacionEmail | NotificacionPush | NotificacionSMS;
function enviarNotificacion(notif: TipoNotificacion): string {
// Usando 'in' para identificar el tipo específico
if ('destinatario' in notif && 'asunto' in notif) {
return `Email enviado a ${notif.destinatario}: ${notif.asunto}`;
}
if ('dispositivos' in notif) {
const badge = notif.badge ? ` (badge: ${notif.badge})` : '';
return `Push enviado a ${notif.dispositivos.length} dispositivos${badge}`;
}
if ('telefono' in notif && 'longitud' in notif) {
return `SMS enviado al ${notif.telefono} (${notif.longitud} caracteres)`;
}
return "Tipo de notificación no soportado";
}
Ventajas del operador in
El operador in
ofrece ventajas específicas sobre otros type guards:
- Flexibilidad: Funciona con cualquier propiedad, no solo métodos o propiedades con valores específicos
- Precisión: Puede distinguir entre objetos con estructuras similares pero propiedades únicas
- Compatibilidad: Es JavaScript nativo, por lo que funciona en cualquier entorno
- Expresividad: El código es claro y autoexplicativo sobre qué propiedad se está verificando
Fuentes y referencias
Documentación oficial y recursos externos para profundizar en TypeScript
Documentación oficial de TypeScript
Alan Sastre
Ingeniero de Software y formador, CEO en CertiDevs
Ingeniero de software especializado en Full Stack y en Inteligencia Artificial. Como CEO de CertiDevs, TypeScript es una de sus áreas de expertise. Con más de 15 años programando, 6K seguidores en LinkedIn y experiencia como formador, Alan se dedica a crear contenido educativo de calidad para desarrolladores de todos los niveles.
Más tutoriales de TypeScript
Explora más contenido relacionado con TypeScript y continúa aprendiendo con nuestros tutoriales gratuitos.
Aprendizajes de esta lección
- Comprender el concepto y utilidad de los type guards en TypeScript.
- Aprender a usar los operadores typeof, instanceof e in para distinguir tipos.
- Crear type guards personalizados para validar estructuras complejas.
- Aplicar type guards en escenarios reales como objetos del DOM, APIs y tipos de unión discriminados.
- Conocer las limitaciones y ventajas de cada técnica para un manejo seguro y eficiente de tipos.