TypeScript tiene tres mecanismos de organización de tipos que no son siempre bien comprendidos ni bien usados: los function overloads para tipar funciones con múltiples firmas, el declaration merging para fusionar definiciones de la misma interfaz o clase y los namespaces para agrupar tipos relacionados sin polucionar el alcance global. Los tres tienen casos de uso legítimos y antipatrones frecuentes que conviene conocer.
Function overloads
Un overload en TypeScript es una firma de tipo (sin cuerpo) seguida de una implementación que debe ser compatible con todas las firmas. TypeScript usa las firmas de overload para la resolución de tipos; la implementación es solo para el runtime:
// Overloads (firmas de tipo):
function parsear(entrada: string): number;
function parsear(entrada: string[]): number[];
function parsear(entrada: string | string[]): number | number[];
// Implementación (una sola, compatible con todas las firmas):
function parsear(entrada: string | string[]): number | number[] {
if (Array.isArray(entrada)) {
return entrada.map(Number);
}
return Number(entrada);
}
const a = parsear("42"); // number
const b = parsear(["1", "2"]); // number[]
Cuándo usar overloads y cuándo no
// Caso donde overloads son la herramienta correcta:
// El tipo de retorno depende del tipo de un argumento de entrada
function obtener(clave: "nombre"): string;
function obtener(clave: "edad"): number;
function obtener(clave: "activo"): boolean;
function obtener(clave: string): string | number | boolean {
const datos: Record<string, string | number | boolean> = {
nombre: "Ana", edad: 30, activo: true
};
return datos[clave];
}
const nombre = obtener("nombre"); // string ?
const edad = obtener("edad"); // number ?
// Antipatrón: overload innecesario cuando basta con un union type
// MAL:
function sumar(a: number, b: number): number;
function sumar(a: string, b: string): string;
function sumar(a: any, b: any) { return a + b; }
// BIEN (más simple):
function sumar<T extends number | string>(a: T, b: T): T {
return (a as any) + b;
}
Declaration merging: fusionar interfaces
TypeScript fusiona automáticamente múltiples declaraciones de la misma interfaz. Esto es la base del module augmentation y permite añadir propiedades a interfaces de terceros:
interface Animal {
nombre: string;
}
interface Animal {
sonido: string;
}
// Resultado de la fusión:
// interface Animal { nombre: string; sonido: string; }
const gato: Animal = { nombre: "Miau", sonido: "Miau" }; // OK
// Nota: las interfaces pueden fusionarse, los tipos (type) no:
type Persona = { nombre: string };
// type Persona = { edad: number }; // Error: identificador duplicado
Declaration merging con funciones y clases
// Una función y un namespace con el mismo nombre se fusionan:
function validar(valor: string): boolean {
return validar.patron.test(valor);
}
namespace validar {
export const patron = /^d+$/;
export function email(valor: string): boolean {
return /^[^@]+@[^@]+.[^@]+$/.test(valor);
}
}
validar("123"); // true (la función)
validar.patron; // el RegExp del namespace
validar.email("[email protected]"); // true (método del namespace)
// Clase + namespace también se fusionan:
class Conexion {
conectar() { /* ... */ }
}
namespace Conexion {
export type Opciones = { host: string; puerto: number };
export const TIMEOUT = 5000;
}
const opciones: Conexion.Opciones = { host: "localhost", puerto: 5432 };
Namespaces: cuándo usarlos
Los namespaces son el mecanismo legacy de TypeScript para organizar código antes de que existieran los módulos ES. Hoy en día no deben usarse para organizar código de aplicación para eso sirven los módulos ES con import/export pero siguen siendo útiles en dos contextos:
// 1. En archivos .d.ts para organizar tipos de librerías:
declare namespace MiLibreria {
interface Opciones {
debug?: boolean;
timeout?: number;
}
function inicializar(opciones?: Opciones): void;
function destruir(): void;
namespace eventos {
function on(evento: string, handler: () => void): void;
function off(evento: string): void;
}
}
// 2. Para agrupar tipos relacionados sin exportarlos individualmente:
namespace Formulario {
export type Estado = "inicial" | "enviando" | "exito" | "error";
export type Campo = { valor: string; error?: string; tocado: boolean };
export type Campos = Record<string, Campo>;
}
function renderizarFormulario(estado: Formulario.Estado): string {
return estado;
}
Los overloads y el declaration merging son herramientas del sistema de tipos de TypeScript sin equivalente en JavaScript. Entender cuándo son la solución correcta (overloads cuando el tipo de retorno depende del tipo del argumento; merging para extensión de módulos) y cuándo no lo son (overloads innecesarios, namespaces para código de aplicación) es parte del TypeScript idiomático.
Imagen: Pexels / Tima Miroshnichenko
