Al diseñar APIs para bibliotecas y SDKs empresariales, los desarrolladores resuelven con frecuencia la necesidad de crear funciones polimórficas cuyo comportamiento y tipo de retorno varíen según el tipo o número de argumentos recibidos. En TypeScript existen dos mecanismos principales para alcanzar este objetivo:
T extends U ? X : Y.Aunque ambos enfoques abordan problemas análogos, operan bajo modelos de cálculo fundamentalmente distintos y difieren drásticamente al procesar tipos de unión (Unions).
+-------------------------------------------------------------------------+
| COMPARACIÓN ARQUITECTÓNICA DE MECANISMOS |
+-------------------------------------------------------------------------+
| [Enfoque 1: Function Overloading] |
| function get(id: string): Entity; |
| function get(ids: string[]): Entity[]; |
| |
| Modelo: Lista lineal discreta. |
| Algoritmo: First-match (de arriba hacia abajo). |
| Comportamiento con Union: ¡FALLA! get(param: string | string[]) -> ERROR!|
+-------------------------------------------------------------------------+
vs
+-------------------------------------------------------------------------+
| [Enfoque 2: Conditional Generics] |
| function get<T extends string | string[]>(idOrIds: T): |
| T extends string[] ? Entity[] : Entity; |
| |
| Modelo: Mapeo matemático distributivo. |
| Algoritmo: Cálculo por proyección de T. |
| Comportamiento con Union: ¡FUNCIONA! Resultado: Entity | Entity[] |
+-------------------------------------------------------------------------+La disparidad entre ambos mecanismos reside en el algoritmo interno de resolución del compilador:
La trampa de las sobrecargas con tipos Union:
Firmas:
1) (x: string) => number
2) (x: number) => string
Llamada:
const arg: string | number = getDynamicValue();
fn(arg); // ERROR TS2769: No overload matches this call.
¿Por qué?
arg no es estrictamente string (podría ser number).
arg no es estrictamente number (podría ser string).
¡El compilador no sintetiza automáticamente una firma de unión combinada!findById('1') -> Entity) o de una lista si se pasa un array (findById(['1', '2']) -> Entity[]).{ suspense: true }.asDate: true.El patrón clásico de sobrecarga destaca por su excelente soporte para documentación JSDoc, proporcionando sugerencias contextuales específicas para cada caso en el editor:
export interface StorageItem {
readonly key: string;
readonly value: string;
}
/**
* Sobrecarga 1: Obtención de una única clave.
* @param key - Identificador de la entrada.
* @returns Cadena de texto o null si la clave no existe.
*/
export function getStorage(key: string): string | null;
/**
* Sobrecarga 2: Lectura por lotes de un conjunto de claves.
* @param keys - Array de identificadores.
* @returns Array de objetos StorageItem.
*/
export function getStorage(keys: ReadonlyArray<string>): ReadonlyArray<StorageItem>;
// Firma de implementación (oculta al exterior)
export function getStorage(
keyOrKeys: string | ReadonlyArray<string>
): string | null | ReadonlyArray<StorageItem> {
if (Array.isArray(keyOrKeys)) {
return keyOrKeys.map((k) => ({ key: k, value: `val_${k}` }));
}
return keyOrKeys === 'missing' ? null : `val_${keyOrKeys}`;
}
// Invocaciones de ejemplo:
const single = getStorage('user_id'); // Tipo: string | null
const multiple = getStorage(['key1', 'key2']); // Tipo: readonly StorageItem[]
// LA TRAMPA DE LA SOBRECARGA:
function processQuery(input: string | string[]) {
// ❌ ERROR DE COMPILACIÓN TS2769: No overload matches this call.
// Overload 1 requires string, Overload 2 requires ReadonlyArray<string>.
// Type 'string | string[]' is not assignable to either.
// const result = getStorage(input);
}Reescribir la misma función mediante un tipo condicional distributivo erradica el problema de los tipos unión:
export interface StorageItem {
readonly key: string;
readonly value: string;
}
export type StorageResult<T extends string | ReadonlyArray<string>> =
T extends ReadonlyArray<string> ? ReadonlyArray<StorageItem> : string | null;
/**
* Firma universal basada en genéricos condicionales.
* Admite tipos concretos así como uniones dinámicas sin fallos.
*/
export function getStorageGeneric<T extends string | ReadonlyArray<string>>(
keyOrKeys: T
): StorageResult<T> {
if (Array.isArray(keyOrKeys)) {
return keyOrKeys.map((k) => ({ key: k, value: `val_${k}` })) as StorageResult<T>;
}
const val = keyOrKeys === 'missing' ? null : `val_${keyOrKeys}`;
return val as StorageResult<T>;
}
// Invocaciones de ejemplo:
const single = getStorageGeneric('user_id'); // Tipo: string | null
const multiple = getStorageGeneric(['k1', 'k2'] as const); // Tipo: readonly StorageItem[]
// EL MANEJO DE UNIONES OPERA SIN ERRORES:
function processQuerySafe(input: string | string[]) {
// ✅ ÉXITO: el resultado se calcula rigurosamente como:
// (string | null) | ReadonlyArray<StorageItem>
const result = getStorageGeneric(input);
}Cuando se requiere simultáneamente documentación JSDoc pormenorizada y soporte para tipos de unión, se añade una firma de sobrecarga dedicada para el tipo unión:
// Firma para elemento individual
export function fetchResource(id: string): Promise<Response>;
// Firma para lista de elementos
export function fetchResource(ids: string[]): Promise<Response[]>;
// Firma de reserva para compatibilidad con uniones (Union Fallback)
export function fetchResource(ids: string | string[]): Promise<Response | Response[]>;
// Implementación
export function fetchResource(
ids: string | string[]
): Promise<Response | Response[]> {
if (Array.isArray(ids)) {
return Promise.all(ids.map((id) => fetch(`/api/${id}`)));
}
return fetch(`/api/${ids}`);
}Determine si el número de parámetros (aridad) varía según el caso. Si difiere (f(a) frente a f(a, b, c)), opte por Function Overloads.
¿Llegarán argumentos en forma de unión (string | string[]) desde el código consumidor? Si es así, las sobrecargas puras causarán errores.
Para un número idéntico de parámetros con dependencia funcional del retorno, utilice T extends Target ? R1 : R2.
Si utiliza genéricos condicionales, extraiga la lógica a un Type Alias legible (StorageResult<T>) para conservar descripciones claras.
export type RawInput = string | number | boolean;
export interface NormalizedResult {
readonly raw: string;
readonly type: 'string' | 'number' | 'boolean';
readonly processedAt: number;
}
export type TransformOutput<T extends RawInput | ReadonlyArray<RawInput>> =
T extends ReadonlyArray<RawInput>
? ReadonlyArray<NormalizedResult>
: NormalizedResult;
/**
* Transformador isomorfo de datos.
* Acepta sin fisuras valores individuales, arrays o uniones dinámicas de ambos.
*/
export function transformInput<T extends RawInput | ReadonlyArray<RawInput>>(
input: T
): TransformOutput<T> {
const normalizeSingle = (val: RawInput): NormalizedResult => ({
raw: String(val),
type: typeof val as NormalizedResult['type'],
processedAt: Date.now(),
});
if (Array.isArray(input)) {
return (input as ReadonlyArray<RawInput>).map(normalizeSingle) as TransformOutput<T>;
}
return normalizeSingle(input as RawInput) as TransformOutput<T>;
}El compilador evalúa las sobrecargas en el orden exacto de su declaración. Situar un tipo más amplio (string o any) antes que un literal concreto ('json') convierte la sobrecarga específica en inalcanzable.
// ❌ ANTIPATRÓN: El string genérico absorbe el literal 'json'
function parseBad(format: string): unknown;
function parseBad(format: 'json'): Record<string, unknown>; // ¡FIRMA INALCANZABLE!
// ✅ CORRECTO: De los tipos más específicos a los más generales
function parseGood(format: 'json'): Record<string, unknown>;
function parseGood(format: string): unknown;Definir sobrecargas sin una firma de reserva genera errores en el código consumidor cuando se suministran valores con tipos de unión dinámicos.
// ❌ ANTIPATRÓN: Sobrecargas sin respaldo para tipos unión
function render(node: string): HTMLElement;
function render(node: number): Text;
function wrapper(val: string | number) {
// Error: No overload matches this call
return render(val);
}
// ✅ CORRECTO: Reemplazar por un genérico condicional
function render<T extends string | number>(node: T): T extends string ? HTMLElement : Text;Incrustar cadenas complejas de operadores ternarios en la propia firma de la función dificulta el mantenimiento. Conviene descomponer los cálculos en Type Aliases auxiliares bien nombrados.
// ❌ ANTIPATRÓN: Espagueti de operadores ternarios directamente en el argumento
function bad<T>(x: T): T extends A ? (T extends B ? C : D) : (T extends E ? F : G) { ... }
// ✅ CORRECTO: Descomposición en tipos utilitarios con nombre
type ResolveOutput<T> = ...;
function good<T>(x: T): ResolveOutput<T> { ... }Si la firma de implementación es demasiado restrictiva o no abarca todos los casos de sobrecarga, TypeScript emite el error TS2394: This overload signature is not compatible with its implementation signature.
// ❌ ANTIPATRÓN: La implementación no contempla la sobrecarga de number
function test(x: string): string;
function test(x: number): number;
function test(x: string): any { ... } // Error TS2394: ¡number no está contemplado!Si los distintos casos reciben cantidades dispares de parámetros, elija Function Overloads.
Si la cantidad de argumentos es uniforme y el retorno depende del tipo de entrada, utilice Generic + Conditional Type.
Si el consumidor puede pasar variables de tipo unión, el tipo condicional es imprescindible para evitar el error TS2769.
Si cada caso exige notas JSDoc particulares, adopte el enfoque híbrido con una sobrecarga explícita para la unión.
Escriba pruebas de tipos comprobando el paso de literales concretos y uniones complejas.