En esta página

Tipado de funciones

14 min lectura TextoCap. 3 — Funciones y genéricos

Tipado de funciones

Las funciones son el elemento central de cualquier programa TypeScript. Tipar correctamente las funciones significa ser preciso sobre qué aceptan y qué prometen devolver, habilitando autocompletado exacto y detección de errores en el punto de uso. Esta lección cubre todos los aspectos del tipado de funciones: expresiones de tipo, call signatures, sobrecargas, parámetros especiales y el parámetro this.

Expresiones de tipo función

La forma más directa de representar una función como tipo es con una expresión de tipo función:

// Tipo función inline
type Comparador = (a: number, b: number) => number;

// Tipo función con nombre explícito
type Transformador<T, U> = (entrada: T) => U;
type Predicado<T> = (elemento: T) => boolean;

// Uso en variables
const ordenarAscendente: Comparador = (a, b) => a - b;
const ordenarDescendente: Comparador = (a, b) => b - a;

// Uso en parámetros
function ordenar<T>(arreglo: T[], comparador: Comparador): T[] {
  return [...arreglo].sort(comparador);
}

Expresiones vs declaraciones

// Declaración de función: hoisted, más legible para funciones nombradas
function calcular(a: number, b: number): number {
  return a + b;
}

// Expresión de función: asignable a variables, ideal para callbacks
const calcular2 = (a: number, b: number): number => a + b;

// Expresión con tipo explícito en la variable (útil para documentar)
const calcular3: (a: number, b: number) => number = (a, b) => a + b;

Call signatures en interfaces

Cuando una función tiene propiedades adicionales (que puede tener en JavaScript), se usa una call signature dentro de una interfaz:

interface FunciónConMetadatos {
  (entrada: string): string;  // Call signature
  descripción: string;        // Propiedad adicional
  versión: number;
}

const transformar: FunciónConMetadatos = (s: string) => s.toUpperCase();
transformar.descripción = 'Transforma texto a mayúsculas';
transformar.versión = 1;

console.log(transformar('hola'));      // 'HOLA'
console.log(transformar.descripción); // 'Transforma texto a mayúsculas'

Call signatures vs expresiones de tipo

// Expresión de tipo: función sin propiedades adicionales
type Fn1 = (x: number) => string;

// Call signature en interface: función que puede tener propiedades
interface Fn2 {
  (x: number): string;
}

// Diferencia: puedes añadir propiedades a Fn2, no a Fn1

Parámetros opcionales y por defecto

// Parámetro opcional: puede no pasarse (tipo: T | undefined internamente)
function saludar(nombre: string, saludo?: string): string {
  return `${saludo ?? 'Hola'}, ${nombre}!`;
}

saludar('Ana');          // 'Hola, Ana!'
saludar('Ana', 'Buenos días'); // 'Buenos días, Ana!'

// Parámetro con valor por defecto: el tipo se infiere del default
function crearUsuario(nombre: string, rol: 'admin' | 'lector' = 'lector') {
  return { nombre, rol };
}

crearUsuario('Ana');           // { nombre: 'Ana', rol: 'lector' }
crearUsuario('Carlos', 'admin'); // { nombre: 'Carlos', rol: 'admin' }

Importante: los parámetros opcionales deben estar al final. Los parámetros con valor por defecto pueden estar en cualquier posición pero es más claro tenerlos al final también.

Parámetros rest tipados

// Rest con tipo primitivo
function unir(separador: string, ...palabras: string[]): string {
  return palabras.join(separador);
}
unir(', ', 'manzana', 'banana', 'cereza'); // 'manzana, banana, cereza'

// Rest con tipo complejo
function registrarEventos(...eventos: Array<{ tipo: string; timestamp: Date }>): void {
  eventos.forEach(e => console.log(`[${e.timestamp.toISOString()}] ${e.tipo}`));
}

// Spread de tupla como argumentos
type ArgsConsola = [mensaje: string, ...datos: unknown[]];
function registrar(...args: ArgsConsola): void {
  const [mensaje, ...datos] = args;
  console.log(mensaje, ...datos);
}

Tipado de callbacks

Los callbacks son uno de los puntos más críticos del tipado de funciones. Un callback mal tipado puede propagar unknown o any por todo el código.

// Callback simple
function procesarArreglo<T>(
  items: T[],
  callback: (item: T, índice: number) => void
): void {
  items.forEach(callback);
}

// Callback que retorna un valor
function mapearArreglo<T, U>(
  items: T[],
  transformar: (item: T) => U
): U[] {
  return items.map(transformar);
}

// Callback asíncrono
async function procesarLote<T>(
  items: T[],
  procesador: (item: T) => Promise<void>,
  concurrencia = 5
): Promise<void> {
  for (let i = 0; i < items.length; i += concurrencia) {
    const lote = items.slice(i, i + concurrencia);
    await Promise.all(lote.map(procesador));
  }
}

// Uso tipado:
procesarArreglo([1, 2, 3], (num, idx) => {
  console.log(`${idx}: ${num}`); // num es number, idx es number
});

Tipo `void` en callbacks: comportamiento especial

Cuando un callback tiene tipo de retorno void, el llamado puede retornar cualquier valor; TypeScript simplemente lo descarta. Esto es diferente al uso de void en funciones normales.

type CallbackVoid = () => void;

// Esto es válido — el valor retornado se ignora
const fn: CallbackVoid = () => 'hola'; // ✅
const fn2: CallbackVoid = () => 42;    // ✅

// Útil para APIs como addEventListener:
const manejador: CallbackVoid = () => { /* sin return */ };
document.addEventListener('click', manejador); // addEventListener espera () => void

Sobrecargas de función

Las sobrecargas permiten definir múltiples firmas para la misma función. TypeScript verifica el tipo correcto según los argumentos en el punto de llamada.

// Firmas de sobrecarga (sin implementación)
function crear(tipo: 'usuario', nombre: string, email: string): { tipo: 'usuario'; nombre: string; email: string };
function crear(tipo: 'producto', nombre: string, precio: number): { tipo: 'producto'; nombre: string; precio: number };

// Implementación (debe ser compatible con todas las sobrecargas)
function crear(
  tipo: 'usuario' | 'producto',
  nombre: string,
  tercero: string | number
): { tipo: 'usuario'; nombre: string; email: string } | { tipo: 'producto'; nombre: string; precio: number } {
  if (tipo === 'usuario') {
    return { tipo: 'usuario', nombre, email: tercero as string };
  }
  return { tipo: 'producto', nombre, precio: tercero as number };
}

// TypeScript infiere el tipo de retorno correcto según la sobrecarga:
const usuario = crear('usuario', 'Ana', '[email protected]');
usuario.email; // ✅ TypeScript sabe que este campo existe

const producto = crear('producto', 'Laptop', 999);
producto.precio; // ✅ TypeScript sabe que este campo existe

Un ejemplo más práctico: la función obtener que retorna tipos distintos según el parámetro:

function obtener(clave: 'nombre'): string;
function obtener(clave: 'edad'): number;
function obtener(clave: 'activo'): boolean;
function obtener(clave: 'nombre' | 'edad' | 'activo'): string | number | boolean {
  const datos = { nombre: 'Ana', edad: 28, activo: true };
  return datos[clave];
}

const nombre = obtener('nombre'); // tipo: string
const edad = obtener('edad');     // tipo: number
const activo = obtener('activo'); // tipo: boolean

Reglas de las sobrecargas

  1. Las firmas de sobrecarga no tienen cuerpo.
  2. La implementación tiene su propia firma más general (no visible externamente).
  3. La firma de implementación debe ser compatible con todas las sobrecargas.
  4. TypeScript usa las sobrecargas en orden (la primera que encaje gana).

El parámetro `this`

JavaScript tiene semántica compleja para this. TypeScript te permite anotar el tipo de this esperado como el primer parámetro (que no es un parámetro real, solo información de tipos):

interface Botón {
  texto: string;
  deshabilitado: boolean;
  onClick(this: Botón): void;
}

const botón: Botón = {
  texto: 'Enviar',
  deshabilitado: false,
  onClick(this: Botón) {
    if (this.deshabilitado) return;
    console.log(`Clic en: ${this.texto}`);
  },
};

botón.onClick(); // ✅
const fn = botón.onClick;
fn(); // ❌ Error: 'this' context is not of type 'Botón'

Esto es especialmente útil cuando compartes métodos entre clases o cuando trabajas con APIs que llaman callbacks con this específicos.

Funciones como parámetros de otras funciones

Los patrones de orden superior son naturales en TypeScript:

// Función que retorna una función (currying manual)
function multiplicarPor(factor: number): (n: number) => number {
  return (n: number) => n * factor;
}

const doble = multiplicarPor(2);
const triple = multiplicarPor(3);
console.log(doble(5));  // 10
console.log(triple(4)); // 12

// Composición de funciones
function componer<A, B, C>(
  f: (b: B) => C,
  g: (a: A) => B
): (a: A) => C {
  return (a: A) => f(g(a));
}

const normalizarTexto = componer(
  (s: string) => s.trim(),
  (s: string) => s.toLowerCase()
);

console.log(normalizarTexto('  HOLA MUNDO  ')); // 'hola mundo'

Funciones asíncronas

Las funciones async siempre retornan una Promise. TypeScript infiere el tipo genérico de la promesa del tipo de retorno de la función:

// El tipo de retorno es Promise<Usuario>
async function obtenerUsuario(id: string): Promise<Usuario> {
  const respuesta = await fetch(`/api/usuarios/${id}`);
  if (!respuesta.ok) {
    throw new Error(`Usuario ${id} no encontrado`);
  }
  return respuesta.json() as Promise<Usuario>;
}

// Funciones que pueden fallar: retornar Resultado en lugar de lanzar
async function obtenerUsuarioSeguro(id: string): Promise<Resultado<Usuario, string>> {
  try {
    const usuario = await obtenerUsuario(id);
    return { ok: true, valor: usuario };
  } catch (err) {
    const mensaje = err instanceof Error ? err.message : 'Error desconocido';
    return { ok: false, error: mensaje };
  }
}

Con el sistema de tipado de funciones dominado, estás listo para dar el siguiente paso: los genéricos. Son el mecanismo que permite reutilizar toda esta lógica tipada con cualquier tipo de datos.

Prefiere call signatures sobre Function
Evita usar el tipo 'Function' (con mayúscula): es demasiado amplio, equivalente a 'any' para funciones. Siempre define la firma exacta: '(nombre: string) => void' o usa un type alias para callbacks reutilizables. Así el llamador sabe exactamente qué pasar.
Sobrecargas para APIs limpias
Las sobrecargas (overloads) te permiten tener múltiples firmas para la misma función según los argumentos. Son útiles para APIs públicas donde quieres documentar exactamente qué tipo retorna para cada combinación de argumentos. La implementación real con lógica compleja queda oculta.
El parámetro this debe ser el primero
Cuando anotas 'this' en una función, debe ser el primer parámetro. TypeScript lo elimina completamente en el JavaScript generado — es puramente información de tipos. No incrementa el número de argumentos reales de la función.