Funciones avanzadas

Avanzado
TypeScript
TypeScript
Actualizado: 27/08/2025

Funciones como tipos y callbacks

En TypeScript, las funciones no son solo código ejecutable, sino que también pueden tratarse como tipos de datos. Esta característica nos permite definir exactamente qué forma debe tener una función, creando contratos que garantizan la seguridad de tipos en nuestro código.

Definición de tipos de función

Una función puede definirse como tipo utilizando la sintaxis de función flecha. Esto nos permite especificar qué parámetros recibe y qué tipo de valor retorna:

// Definir un tipo de función
type Calculadora = (a: number, b: number) => number;

// Implementar funciones que cumplan este tipo
const sumar: Calculadora = (x, y) => x + y;
const multiplicar: Calculadora = (x, y) => x * y;

También podemos usar la sintaxis de interfaz para definir tipos de función más complejos:

interface ValidadorTexto {
  (texto: string): boolean;
}

const validarEmail: ValidadorTexto = (email) => {
  return email.includes('@') && email.includes('.');
};

const validarPassword: ValidadorTexto = (password) => {
  return password.length >= 8;
};

Funciones que reciben otras funciones

Los callbacks son funciones que se pasan como parámetros a otras funciones. En TypeScript, debemos especificar el tipo exacto del callback que esperamos recibir:

// Función que recibe un callback como parámetro
function procesarNumeros(
  numeros: number[], 
  callback: (num: number) => number
): number[] {
  return numeros.map(callback);
}

// Usar la función con diferentes callbacks
const duplicar = (n: number) => n * 2;
const elevarAlCuadrado = (n: number) => n * n;

const resultado1 = procesarNumeros([1, 2, 3], duplicar);        // [2, 4, 6]
const resultado2 = procesarNumeros([1, 2, 3], elevarAlCuadrado); // [1, 4, 9]

Callbacks con múltiples parámetros

Los callbacks pueden recibir múltiples parámetros y tener diferentes tipos de retorno:

// Callback que recibe índice y valor
function filtrarConIndice<T>(
  array: T[], 
  callback: (item: T, index: number) => boolean
): T[] {
  return array.filter(callback);
}

const numeros = [10, 20, 30, 40, 50];

// Filtrar elementos en posiciones pares
const enPosicionesPares = filtrarConIndice(numeros, (valor, indice) => {
  return indice % 2 === 0;
});

console.log(enPosicionesPares); // [10, 30, 50]

Callbacks opcionales

En ocasiones, los callbacks son opcionales. Utilizamos el operador ? para indicar esto y debemos verificar su existencia antes de ejecutarlos:

interface OpcionesOperacion {
  onSuccess?: (resultado: number) => void;
  onError?: (error: string) => void;
}

function realizarOperacion(
  a: number, 
  b: number, 
  operacion: string,
  opciones: OpcionesOperacion = {}
): void {
  try {
    let resultado: number;
    
    switch (operacion) {
      case 'suma':
        resultado = a + b;
        break;
      case 'division':
        if (b === 0) throw new Error('División por cero');
        resultado = a / b;
        break;
      default:
        throw new Error('Operación no válida');
    }
    
    // Ejecutar callback de éxito si existe
    opciones.onSuccess?.(resultado);
    
  } catch (error) {
    // Ejecutar callback de error si existe
    opciones.onError?.(error.message);
  }
}

// Uso con callbacks
realizarOperacion(10, 2, 'division', {
  onSuccess: (resultado) => console.log(`Resultado: ${resultado}`),
  onError: (error) => console.error(`Error: ${error}`)
});

Tipos de función inline

Para casos simples, podemos definir tipos de función directamente en el parámetro sin crear tipos separados:

// Función que ordena un array usando un callback comparador
function ordenarPersonalizado<T>(
  array: T[],
  comparador: (a: T, b: T) => number
): T[] {
  return array.sort(comparador);
}

const productos = [
  { nombre: 'Laptop', precio: 999 },
  { nombre: 'Mouse', precio: 25 },
  { nombre: 'Teclado', precio: 75 }
];

// Ordenar por precio ascendente
const porPrecio = ordenarPersonalizado(productos, (a, b) => a.precio - b.precio);

// Ordenar por nombre alfabéticamente
const porNombre = ordenarPersonalizado(productos, (a, b) => 
  a.nombre.localeCompare(b.nombre)
);

Funciones que retornan funciones

Las funciones también pueden retornar otras funciones, creando lo que se conoce como funciones de orden superior:

// Función que crea un validador personalizado
function crearValidador(longitudMinima: number): (texto: string) => boolean {
  return function(texto: string): boolean {
    return texto.length >= longitudMinima;
  };
}

// Crear diferentes validadores
const validarNombre = crearValidador(2);
const validarPassword = crearValidador(8);
const validarComentario = crearValidador(10);

console.log(validarNombre('Juan'));      // true
console.log(validarPassword('123'));     // false
console.log(validarComentario('Hola!')); // false

Esta aproximación nos permite crear factorías de funciones que generan callbacks especializados según nuestras necesidades:

// Factory para crear diferentes tipos de filtros
function crearFiltro<T>(
  criterio: keyof T
): (valor: T[keyof T]) => (item: T) => boolean {
  return function(valor: T[keyof T]) {
    return function(item: T): boolean {
      return item[criterio] === valor;
    };
  };
}

interface Producto {
  categoria: string;
  precio: number;
  disponible: boolean;
}

const productos: Producto[] = [
  { categoria: 'electronica', precio: 299, disponible: true },
  { categoria: 'ropa', precio: 49, disponible: false },
  { categoria: 'electronica', precio: 599, disponible: true }
];

// Crear filtro por categoría
const filtrarPorCategoria = crearFiltro<Producto>('categoria');
const soloElectronica = filtrarPorCategoria('electronica');

const productosElectronicos = productos.filter(soloElectronica);
console.log(productosElectronicos); // Solo productos de electrónica

El uso de funciones como tipos y callbacks en TypeScript nos proporciona una base sólida para crear código más modular y reutilizable, manteniendo la seguridad de tipos que caracteriza al lenguaje.

Sobrecarga de funciones

La sobrecarga de funciones en TypeScript permite definir múltiples firmas para una sola función, proporcionando diferentes formas de llamarla según los tipos y número de parámetros. Esta característica mejora la flexibilidad del código manteniendo la seguridad de tipos.

Concepto básico de sobrecarga

Una función sobrecargada se define mediante múltiples declaraciones de firma seguidas de una implementación única que debe ser compatible con todas las firmas declaradas:

// Firmas de la función sobrecargada
function formatear(valor: string): string;
function formatear(valor: number): string;
function formatear(valor: boolean): string;

// Implementación que debe manejar todos los casos
function formatear(valor: string | number | boolean): string {
  if (typeof valor === 'string') {
    return valor.toUpperCase();
  }
  if (typeof valor === 'number') {
    return valor.toFixed(2);
  }
  return valor ? 'SÍ' : 'NO';
}

// Uso de la función sobrecargada
const texto = formatear('hola');        // "HOLA"
const numero = formatear(3.14159);      // "3.14"
const booleano = formatear(true);       // "SÍ"

Sobrecarga con diferentes números de parámetros

La sobrecarga permite que una función acepte diferentes cantidades de parámetros, cada una con su comportamiento específico:

// Diferentes firmas con distinto número de parámetros
function crear(nombre: string): Usuario;
function crear(nombre: string, edad: number): Usuario;
function crear(nombre: string, edad: number, email: string): Usuario;

// Implementación unificada
function crear(
  nombre: string, 
  edad?: number, 
  email?: string
): Usuario {
  const usuario: Usuario = { nombre };
  
  if (edad !== undefined) {
    usuario.edad = edad;
  }
  
  if (email !== undefined) {
    usuario.email = email;
  }
  
  return usuario;
}

interface Usuario {
  nombre: string;
  edad?: number;
  email?: string;
}

// Diferentes formas de uso
const usuario1 = crear('Ana');                    // Solo nombre
const usuario2 = crear('Carlos', 30);             // Nombre y edad  
const usuario3 = crear('María', 25, 'maria@example.com'); // Todos los datos

Sobrecarga con tipos de retorno diferentes

Las funciones sobrecargadas pueden retornar diferentes tipos según los parámetros recibidos:

// Sobrecarga que retorna diferentes tipos
function obtener(id: number): Usuario | null;
function obtener(filtro: string): Usuario[];
function obtener(activo: boolean): Usuario[];

// Implementación que maneja todos los casos
function obtener(parametro: number | string | boolean): Usuario | Usuario[] | null {
  if (typeof parametro === 'number') {
    // Buscar por ID - retorna un usuario o null
    const usuario = baseDatos.find(u => u.id === parametro);
    return usuario || null;
  }
  
  if (typeof parametro === 'string') {
    // Filtrar por texto - retorna array
    return baseDatos.filter(u => 
      u.nombre.toLowerCase().includes(parametro.toLowerCase())
    );
  }
  
  // Filtrar por estado activo - retorna array
  return baseDatos.filter(u => u.activo === parametro);
}

const baseDatos: (Usuario & { id: number; activo: boolean })[] = [
  { id: 1, nombre: 'Ana', activo: true },
  { id: 2, nombre: 'Carlos', activo: false }
];

// TypeScript infiere correctamente el tipo de retorno
const unUsuario = obtener(1);           // Usuario | null
const usuariosTexto = obtener('ana');   // Usuario[]
const usuariosActivos = obtener(true);  // Usuario[]

Sobrecarga de métodos en clases

Los métodos de clase también pueden ser sobrecargados siguiendo el mismo patrón:

class CalculadoraAvanzada {
  // Sobrecargas del método calcular
  calcular(operacion: 'suma', a: number, b: number): number;
  calcular(operacion: 'potencia', base: number, exponente: number): number;
  calcular(operacion: 'raiz', numero: number): number;
  
  // Implementación del método
  calcular(
    operacion: 'suma' | 'potencia' | 'raiz',
    a: number,
    b?: number
  ): number {
    switch (operacion) {
      case 'suma':
        if (b === undefined) {
          throw new Error('La suma requiere dos números');
        }
        return a + b;
        
      case 'potencia':
        if (b === undefined) {
          throw new Error('La potencia requiere base y exponente');
        }
        return Math.pow(a, b);
        
      case 'raiz':
        return Math.sqrt(a);
        
      default:
        throw new Error('Operación no válida');
    }
  }
}

const calc = new CalculadoraAvanzada();

const suma = calc.calcular('suma', 5, 3);      // 8
const potencia = calc.calcular('potencia', 2, 3); // 8
const raiz = calc.calcular('raiz', 16);        // 4

Sobrecarga con parámetros opcionales complejos

Para casos más sofisticados, podemos crear sobrecargas que manejen objetos de configuración opcionales:

// Diferentes formas de configurar una consulta
function consultar(tabla: string): Promise<any[]>;
function consultar(tabla: string, limite: number): Promise<any[]>;
function consultar(tabla: string, config: ConfigConsulta): Promise<any[]>;

interface ConfigConsulta {
  limite?: number;
  orden?: string;
  filtros?: Record<string, any>;
}

// Implementación que maneja todas las variantes
async function consultar(
  tabla: string,
  configOLimite?: number | ConfigConsulta
): Promise<any[]> {
  let config: ConfigConsulta = {};
  
  if (typeof configOLimite === 'number') {
    config.limite = configOLimite;
  } else if (configOLimite) {
    config = configOLimite;
  }
  
  // Simular consulta a base de datos
  console.log(`Consultando tabla: ${tabla}`, config);
  return []; // Simulación
}

// Diferentes formas de uso
const todos = await consultar('usuarios');
const limitados = await consultar('usuarios', 10);
const complejos = await consultar('usuarios', {
  limite: 5,
  orden: 'nombre',
  filtros: { activo: true }
});

Mejores prácticas para sobrecarga

Al implementar sobrecarga de funciones, es importante seguir ciertas prácticas recomendadas:

// ✅ BUENA PRÁCTICA: Sobrecargas claras y específicas
function procesar(datos: string[]): string;
function procesar(datos: number[]): number;
function procesar(datos: (string | number)[]): string | number {
  if (typeof datos[0] === 'string') {
    return (datos as string[]).join(', ');
  }
  return (datos as number[]).reduce((sum, num) => sum + num, 0);
}

// ✅ BUENA PRÁCTICA: Validación en la implementación
function convertir(valor: string): number;
function convertir(valor: number): string;
function convertir(valor: string | number): string | number {
  if (typeof valor === 'string') {
    const numero = parseFloat(valor);
    if (isNaN(numero)) {
      throw new Error('No se puede convertir a número');
    }
    return numero;
  }
  return valor.toString();
}

// ✅ BUENA PRÁCTICA: Documentación clara
/**
 * Busca elementos en una colección
 * @param id - Buscar por ID único
 * @param filtro - Buscar por texto en nombre
 * @returns Un elemento o array de elementos
 */
function buscar(id: number): Elemento | null;
function buscar(filtro: string): Elemento[];
function buscar(criterio: number | string): Elemento | Elemento[] | null {
  // Implementación...
  return null; // Simulación
}

interface Elemento {
  id: number;
  nombre: string;
}

La sobrecarga de funciones es una herramienta valiosa que permite crear APIs más expresivas y fáciles de usar, proporcionando múltiples formas intuitivas de interactuar con nuestras funciones mientras mantenemos la seguridad de tipos característica de TypeScript.

Funciones genéricas básicas

Las funciones genéricas permiten escribir código reutilizable que funciona con múltiples tipos de datos, manteniendo la información del tipo específico utilizado en cada llamada. Esto nos ayuda a crear funciones más flexibles sin perder la seguridad de tipos.

Introducción a los genéricos en funciones

Una función genérica utiliza parámetros de tipo que actúan como marcadores de posición para los tipos reales que se proporcionarán cuando se llame a la función:

// Función genérica básica
function identidad<T>(valor: T): T {
  return valor;
}

// TypeScript infiere automáticamente el tipo
const numeroResult = identidad(42);        // T se infiere como number
const textoResult = identidad("Hola");     // T se infiere como string
const boolResult = identidad(true);        // T se infiere como boolean

// También podemos especificar el tipo explícitamente
const explicito = identidad<string>("TypeScript");

El parámetro de tipo <T> se coloca entre el nombre de la función y los parámetros. La T es una convención que significa "Type", aunque podemos usar cualquier nombre.

Funciones genéricas con múltiples tipos

Las funciones pueden trabajar con varios parámetros de tipo simultáneamente:

// Función con múltiples tipos genéricos
function combinar<T, U>(primero: T, segundo: U): [T, U] {
  return [primero, segundo];
}

// Diferentes combinaciones de tipos
const numeroTexto = combinar(100, "puntos");        // [number, string]
const textoBoolean = combinar("activo", true);      // [string, boolean]
const fechaNumero = combinar(new Date(), 2024);     // [Date, number]

// Función que intercambia valores en un objeto
function intercambiar<T, U>(objeto: { a: T; b: U }): { a: U; b: T } {
  return {
    a: objeto.b,
    b: objeto.a
  };
}

const original = { a: "primera", b: 123 };
const intercambiado = intercambiar(original); // { a: number; b: string }

Restricciones en funciones genéricas

Podemos aplicar restricciones a los tipos genéricos usando la palabra clave extends para limitar qué tipos son aceptables:

// Restricción: T debe tener una propiedad length
function obtenerLongitud<T extends { length: number }>(item: T): number {
  return item.length;
}

// Funciona con strings, arrays, etc.
const longitudTexto = obtenerLongitud("Hola mundo");     // 10
const longitudArray = obtenerLongitud([1, 2, 3, 4]);     // 4
const longitudPersonalizado = obtenerLongitud({ length: 5, data: "test" }); // 5

// ❌ Error: number no tiene propiedad length
// const error = obtenerLongitud(123);

Restricciones más específicas nos permiten trabajar con propiedades conocidas del tipo:

// Restricción: T debe extender de un objeto con propiedades específicas
interface Nombrable {
  nombre: string;
}

function saludar<T extends Nombrable>(objeto: T): string {
  return `Hola, ${objeto.nombre}`;
}

// Funciona con cualquier objeto que tenga la propiedad nombre
const usuario = { nombre: "Ana", edad: 30 };
const producto = { nombre: "Laptop", precio: 999, categoria: "tech" };

const saludo1 = saludar(usuario);    // "Hola, Ana"
const saludo2 = saludar(producto);   // "Hola, Laptop"

Funciones genéricas para manipulación de arrays

Los genéricos son especialmente útiles para crear funciones de utilidad que trabajen con arrays de cualquier tipo:

// Función genérica para filtrar arrays
function filtrar<T>(
  array: T[], 
  predicado: (item: T) => boolean
): T[] {
  return array.filter(predicado);
}

// Uso con diferentes tipos de arrays
const numeros = [1, 2, 3, 4, 5, 6];
const pares = filtrar(numeros, n => n % 2 === 0);  // [2, 4, 6]

const palabras = ["casa", "auto", "bicicleta", "avión"];
const palabrasCortas = filtrar(palabras, p => p.length <= 4);  // ["casa", "auto"]

// Función genérica para obtener el primer elemento que cumple una condición
function buscarPrimero<T>(
  array: T[], 
  condicion: (item: T) => boolean
): T | undefined {
  return array.find(condicion);
}

interface Persona {
  nombre: string;
  edad: number;
}

const personas: Persona[] = [
  { nombre: "Juan", edad: 25 },
  { nombre: "María", edad: 30 },
  { nombre: "Carlos", edad: 28 }
];

const personaMayor = buscarPrimero(personas, p => p.edad > 27);
// Resultado: { nombre: "María", edad: 30 } | undefined

Funciones genéricas con valores por defecto

Podemos establecer tipos por defecto para los parámetros genéricos:

// Función genérica con tipo por defecto
function crearRespuesta<T = string>(
  datos: T, 
  exitoso: boolean = true
): { datos: T; exitoso: boolean; timestamp: Date } {
  return {
    datos,
    exitoso,
    timestamp: new Date()
  };
}

// Si no especificamos el tipo, usa string por defecto
const respuestaTexto = crearRespuesta("Operación completada");
// Tipo inferido: { datos: string; exitoso: boolean; timestamp: Date }

// Podemos especificar un tipo diferente
const respuestaNumero = crearRespuesta<number>(42);
// Tipo: { datos: number; exitoso: boolean; timestamp: Date }

const respuestaObjeto = crearRespuesta({ usuario: "Ana", id: 123 });
// TypeScript infiere el tipo del objeto automáticamente

Funciones genéricas que retornan promesas

Las funciones genéricas son muy útiles cuando trabajamos con operaciones asíncronas:

// Función genérica que simula una petición HTTP
async function obtenerDatos<T>(url: string): Promise<T> {
  // Simulamos una petición HTTP
  return new Promise((resolve) => {
    setTimeout(() => {
      // En una implementación real, aquí haríamos fetch()
      const datos = `Datos de ${url}` as unknown as T;
      resolve(datos);
    }, 1000);
  });
}

// Uso con diferentes tipos esperados
interface Usuario {
  id: number;
  nombre: string;
  email: string;
}

interface Producto {
  id: number;
  titulo: string;
  precio: number;
}

// TypeScript sabe exactamente qué tipo retorna cada llamada
async function ejemploUso() {
  const usuario = await obtenerDatos<Usuario>("/api/usuarios/1");
  // usuario tiene tipo Usuario
  
  const productos = await obtenerDatos<Producto[]>("/api/productos");
  // productos tiene tipo Producto[]
  
  console.log(usuario.nombre);     // TypeScript sabe que existe la propiedad nombre
  console.log(productos[0].precio); // TypeScript sabe que existe la propiedad precio
}

Funciones genéricas con restricciones de clave

Una técnica avanzada pero muy útil es usar keyof para restringir los parámetros a las claves de un objeto:

// Función genérica que extrae propiedades de objetos
function obtenerPropiedad<T, K extends keyof T>(objeto: T, clave: K): T[K] {
  return objeto[clave];
}

const persona = {
  nombre: "Elena",
  edad: 28,
  activo: true,
  hobbies: ["lectura", "natación"]
};

// TypeScript asegura que la clave existe y retorna el tipo correcto
const nombre = obtenerPropiedad(persona, "nombre");     // string
const edad = obtenerPropiedad(persona, "edad");         // number
const hobbies = obtenerPropiedad(persona, "hobbies");   // string[]

// ❌ Error: 'altura' no existe en el tipo del objeto
// const error = obtenerPropiedad(persona, "altura");

Funciones genéricas en expresiones

Las funciones flecha también pueden ser genéricas utilizando una sintaxis ligeramente diferente:

// Función flecha genérica
const mapear = <T, U>(array: T[], transformador: (item: T) => U): U[] => {
  return array.map(transformador);
};

// Uso de la función flecha genérica
const numeros = [1, 2, 3, 4, 5];
const cuadrados = mapear(numeros, n => n * n);           // number[]
const textos = mapear(numeros, n => `Número: ${n}`);     // string[]

// Función genérica que crea un validador
const crearValidadorGenerico = <T>(
  validador: (item: T) => boolean
) => {
  return (item: T): { valido: boolean; valor: T } => ({
    valido: validador(item),
    valor: item
  });
};

const validarPositivo = crearValidadorGenerico<number>(n => n > 0);
const validarNoVacio = crearValidadorGenerico<string>(s => s.length > 0);

const resultado1 = validarPositivo(-5);    // { valido: false, valor: -5 }
const resultado2 = validarNoVacio("test"); // { valido: true, valor: "test" }

Fuentes y referencias

Documentación oficial y recursos externos para profundizar en TypeScript

Documentación oficial de TypeScript
Alan Sastre - Autor del tutorial

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 cómo definir funciones como tipos y utilizar callbacks con parámetros y retornos específicos.
  • Aprender a implementar sobrecarga de funciones para manejar múltiples firmas y comportamientos.
  • Conocer el uso de funciones genéricas para crear código reutilizable y seguro con múltiples tipos.
  • Aplicar restricciones y valores por defecto en funciones genéricas para mayor flexibilidad.
  • Entender cómo combinar funciones genéricas con operaciones asíncronas y técnicas avanzadas como restricciones de clave.