🚨 ¡Nueva review! ¡Mi teclado ideal! ⌨️ Perfecto para programar, el Logitech MX Keys S . ¡Échale un ojo! 👀

Cómo tipar funciones en TypeScript como un profesional

Parámetros, retornos, callbacks, void y never

Escrito por domin el 15 de marzo de 2026

Vamos con el tercer post de la serie de TypeScript. Si no has leído los anteriores, pásate primero por ¿Qué es TypeScript? y Tipos básicos e Inferencia porque aquí damos por sentado que ya controlas lo básico.

Hoy toca funciones. Y no las funciones cuñadas de JavaScript donde le pasas lo que te da la gana y devuelve lo que le apetece. No. Hoy vas a aprender a tipar funciones como un profesional: parámetros, retornos, callbacks y esa duda eterna entre void y never. EA, vamos al lío.

Logo de TypeScript sobre fondo azul con código.

1. Tipando parámetros: la primera línea de defensa

Aquí viene el primer concepto importante que es que los parámetros de una función NO se infieren. A diferencia de las variables donde TypeScript deduce el tipo del valor asignado, los parámetros llegan de fuera y TypeScript no sabe qué son.

// ❌ Sin tipar: los parámetros son 'any' implícito
// Con strict mode activado, esto DA ERROR directamente
function sumar(a, b) {
    return a + b;
}
// Error: Parameter 'a' implicitly has an 'any' type
// Error: Parameter 'b' implicitly has an 'any' type

¿Por qué? Porque TypeScript no puede adivinar con qué vas a llamar a sumar(). ¿Con números? ¿Con strings? ¿Con un objeto y un array? No lo sabe. Y en modo , no te deja pasar con ese any implícito.

La solución es que le digas a TypeScript qué esperas:

// ✅ Parámetros tipados: ahora TypeScript sabe qué entra
function sumar(a: number, b: number) {
    return a + b;
}

sumar(5, 3);       // ✅ Perfecto
sumar('5', 3);     // ❌ Error: Argument of type 'string' is not assignable
sumar(5);          // ❌ Error: Expected 2 arguments, but got 1
sumar(5, 3, 1);    // ❌ Error: Expected 2 arguments, but got 3

TypeScript no solo valida los tipos, también valida la cantidad de argumentos. En JavaScript puedes llamar a una función con más o menos argumentos de los que espera y no pasa nada. En TypeScript, si la función espera 2, le pasas 2, E YA.

Parámetros opcionales y valores por defecto

A veces un parámetro no es obligatorio. Para eso tienes dos opciones:

// Opción 1: Parámetro opcional con ?
function saludar(nombre: string, titulo?: string) {
    if (titulo) {
        return `Hola, ${titulo} ${nombre}`;
    }
    return `Hola, ${nombre}`;
}

saludar('Domin');              // ✅ "Hola, Domin"
saludar('Domin', 'Sr.');      // ✅ "Hola, Sr. Domin"

// Opción 2: Valor por defecto (se infiere el tipo automáticamente)
function saludarConDefault(nombre: string, titulo = 'Crack') {
    return `Hola, ${titulo} ${nombre}`;
}

saludarConDefault('Domin');         // ✅ "Hola, Crack Domin"
saludarConDefault('Domin', 'Don');  // ✅ "Hola, Don Domin"

Con el valor por defecto, TypeScript si infiere que titulo es string porque 'Crack' es un string. No necesitas escribir : string. La inferencia trabaja por ti, como vimos en el post anterior.

❓ Parámetro opcional (?)

El tipo es string | undefined. Puede no venir y necesitas comprobar antes de usar.

✅ Valor por defecto (=)

Siempre tiene un valor. Si no le pasas nada, usa el default. No necesitas comprobar nada.


2. Tipando el retorno: ¿necesario o redundante?

TypeScript puede inferir lo que devuelve una función mirando el return. Pero, ¿deberías confiar en eso o escribir el tipo de retorno siempre?

// TypeScript infiere que retorna number
function multiplicar(a: number, b: number) {
    return a * b;
}

// Aquí escribimos el tipo de retorno explícitamente
function multiplicarExplicito(a: number, b: number): number {
    return a * b;
}

Las dos son equivalentes. Entonces, ¿cuándo escribir el retorno explícitamente? Aquí va la guía:

Escribe el tipo de retorno cuando...
  • Es una función pública o de una API/librería
  • La función es larga y el retorno no es obvio a simple vista
  • Quieres que el compilador te avise si rompes el contrato
  • La función tiene múltiples returns con tipos distintos
  • Trabajas en equipo y quieres documentar la intención
Deja que TypeScript lo infiera cuando...
  • Es una función corta y simple (una línea)
  • Es una función interna/privada que solo usas tú
  • El retorno es obvio por el código (ej: a + b con numbers)
  • Es un callback inline dentro de un .map() o .filter()

¿Por qué escribir el retorno protege tu código?

Mira este ejemplo donde el retorno explícito te salva de un bug sutil:

// ❌ Sin tipo de retorno: TypeScript no te avisa del error
function buscarUsuario(id: number) {
    if (id === 1) {
        return { nombre: 'Domin', edad: 28 };
    }
    // Oops... olvidé el return para otros casos
    // TypeScript infiere: { nombre: string; edad: number } | undefined
    // Y tú ni te enteras
}

// ✅ Con tipo de retorno: TypeScript te grita
interface Usuario {
    nombre: string;
    edad: number;
}

function buscarUsuarioSeguro(id: number): Usuario {
    if (id === 1) {
        return { nombre: 'Domin', edad: 28 };
    }
    // ❌ Error: A function whose declared type is 'Usuario'
    //    must return a value.
    // ¡TypeScript te obliga a manejar TODOS los caminos!
}

Al declarar el retorno como Usuario, TypeScript te obliga a devolver un Usuario en todos los caminos posibles de la función. Si te olvidas de un return, te lo dice. Ojito eh, esto en proyectos grandes es genial.


3. Arrow functions: la misma historia, más compacta

Todo lo que hemos visto aplica igual a las arrow functions . La sintaxis es un pelín diferente pero el tipado es idéntico:

// Function declaration
function sumar(a: number, b: number): number {
    return a + b;
}

// Arrow function (equivalente)
const sumar = (a: number, b: number): number => {
    return a + b;
};

// Arrow function con retorno implícito (una línea)
const sumar = (a: number, b: number): number => a + b;

// Y dejando que infiera el retorno (también válido):
const sumar = (a: number, b: number) => a + b;

Las cuatro hacen lo mismo. La última es la más compacta y TypeScript infiere que devuelve number. Elige la que te resulte más legible según el contexto.

Tipar la variable completa con Type Alias

También puedes definir el tipo completo de la función por separado y luego asignarlo:

// Definimos el tipo de la función
type OperacionMatematica = (a: number, b: number) => number;

// Ahora la usamos (los parámetros se infieren del tipo)
const sumar: OperacionMatematica = (a, b) => a + b;
const restar: OperacionMatematica = (a, b) => a - b;
const multiplicar: OperacionMatematica = (a, b) => a * b;

// ❌ Error: Type '(a: number, b: number) => string' is not assignable
const dividir: OperacionMatematica = (a, b) => `${a / b}`;

Defines el contrato una vez y lo reutilizas en todas las funciones que lo cumplan. Si una función devuelve string en vez de number, TypeScript te dice NOOUP!


4. Callbacks: funciones que reciben funciones

Con esto es cuando se puede liar un poquitín. Cuando una función recibe otra función como parámetro (un ), necesitas tipar ese parámetro, y aquí viene el error clásico:

❌ El error del novato: usar Function

// ❌ MAL: El tipo genérico Function
function ejecutarAccion(callback: Function) {
    callback(); // TypeScript no sabe qué parámetros acepta
                 // ni qué devuelve. Es como usar 'any' para funciones
}

ejecutarAccion(42);            // ❌ No da error... pero debería
ejecutarAccion('hola');        // ❌ Tampoco da error... wtf
ejecutarAccion(() => 'mundo'); // ✅ Esto sí es una función

Usar Function es el equivalente a usar any pero para funciones. TypeScript no comprueba nada, ni los parámetros, ni el retorno, ni si realmente le estás pasando una función o un churro, hay que evitar esto.

✅ La forma correcta: tipar con arrow function

// ✅ BIEN: Definimos exactamente qué forma tiene el callback
function ejecutarAccion(callback: () => void) {
    callback();
}

ejecutarAccion(() => console.log('¡Hecho!')); // ✅
ejecutarAccion(42);                            // ❌ Error de tipo

Ahora TypeScript sabe que callback es una función que no recibe parámetros y no devuelve nada (void) y si le pasas algo que no cumple eso peta.

Callbacks con parámetros

// Callback que recibe un string y no devuelve nada
function procesarNombre(nombre: string, callback: (resultado: string) => void) {
    const mayusculas = nombre.toUpperCase();
    callback(mayusculas);
}

procesarNombre('domin', (resultado) => {
    console.log(resultado); // "DOMIN"
    // 'resultado' se infiere como string automáticamente 🎉
});

// Callback que recibe dos numbers y devuelve un number
function operar(a: number, b: number, operacion: (x: number, y: number) => number) {
    return operacion(a, b);
}

operar(10, 5, (x, y) => x + y);  // ✅ 15
operar(10, 5, (x, y) => x - y);  // ✅ 5
operar(10, 5, (x, y) => `${x}`); // ❌ Error: Type 'string' is not assignable to type 'number'

Dentro del callback de procesarNombre, TypeScript infiere que resultado es string sin que tú lo escribas. ¿Por qué? Porque la definición del tipo ya dice que el callback recibe un (resultado: string) y la inferencia trabaja incluso dentro de callbacks.

Comparativa Function vs Arrow Type

CaracterísticaFunction (❌)(params) => return (✅)
Valida parámetrosNoSí, tipo y cantidad
Valida retornoNoSí, tipo exacto
AutocompletadoNingunoCompleto dentro del callback
Inferencia en paramsNoSí, automática
SeguridadNula (como any)Total

5. La diferencia entre void y never

Esta es la duda que tiene todo el mundo al empezar con TypeScript. Son parecidos pero significan cosas completamente distintas:

✅ void

La función termina normalmente pero no devuelve nada útil. Es como decir "hago mi trabajo y me voy". Ejemplo: un console.log, guardar en base de datos...

🚫 never

La función NUNCA termina. Ni devuelve, ni acaba. O lanza una excepción, o entra en un bucle infinito. El flujo del programa se corta ahí.

void en acción

// Esta función TERMINA, pero no devuelve nada útil
function logMensaje(mensaje: string): void {
    console.log(`[LOG]: ${mensaje}`);
    // No hay return (o hay un return sin valor)
    // La función termina normalmente
}

// También puedes hacer return sin valor
function guardarEnBD(dato: string): void {
    if (!dato) return; // Sale antes, pero sigue siendo void
    // ... guardar en base de datos
}

// void permite que ignores el valor de retorno
const resultado = logMensaje('hola');
// resultado es de tipo 'void' — no puedes hacer nada útil con él

never en acción

// Esta función NUNCA termina: siempre lanza un error
function lanzarError(mensaje: string): never {
    throw new Error(mensaje);
    // El código NUNCA pasa de aquí
    // No hay return posible
}

// Esta función NUNCA termina: bucle infinito
function bucleInfinito(): never {
    while (true) {
        // Hace algo eternamente (ej: un event loop, un worker)
    }
    // Jamás llega aquí
}

// Uso práctico: exhaustive checking
type Color = 'rojo' | 'verde' | 'azul';

function procesarColor(color: Color): string {
    switch (color) {
        case 'rojo':
            return '#FF0000';
        case 'verde':
            return '#00FF00';
        case 'azul':
            return '#0000FF';
        default:
            // Si alguien añade un nuevo color al tipo Color
            // y no lo handlea aquí, TypeScript dará error
            const _exhaustive: never = color;
            throw new Error(`Color no esperado: ${color}`);
    }
}

El truco del es genial porque si mañana alguien añade 'amarillo' al tipo Color y no actualiza el switch, TypeScript dará error porque 'amarillo' no se puede asignar a never. Te obliga a manejar todos los casos. Esto en proyectos reales evita bugs que de otra forma pasarían desapercibidos.

Resumen visual

Conceptovoidnever
¿La función termina?Sí, normalmenteNo, NUNCA
¿Tiene return?No, o return vacíoImposible (throw o bucle ∞)
Ejemplo típicoconsole.log, guardar datosthrow Error, while(true)
¿Se puede usar el resultado?No (es undefined)No existe resultado
¿Cuándo usarlo?Funciones que hacen algo pero no devuelvenFunciones que cortan el flujo o no terminan

6. Patrones reales: todo junto

Vamos a juntar todo lo que hemos aprendido en un ejemplo más realista. Imagina un mini sistema de eventos:

// Tipo para los callbacks de eventos
type EventCallback = (data: unknown) => void;

// Tipo para el sistema de eventos
interface EventSystem {
    on: (evento: string, callback: EventCallback) => void;
    emit: (evento: string, data: unknown) => void;
    off: (evento: string, callback: EventCallback) => void;
}

// Implementación
function crearEventSystem(): EventSystem {
    const listeners = new Map<string, EventCallback[]>();

    return {
        on(evento: string, callback: EventCallback): void {
            const actuales = listeners.get(evento) || [];
            actuales.push(callback);
            listeners.set(evento, actuales);
        },

        emit(evento: string, data: unknown): void {
            const callbacks = listeners.get(evento) || [];
            callbacks.forEach(cb => cb(data));
        },

        off(evento: string, callback: EventCallback): void {
            const actuales = listeners.get(evento) || [];
            listeners.set(
                evento,
                actuales.filter(cb => cb !== callback)
            );
        },
    };
}

// Uso
const eventos = crearEventSystem();

const miCallback = (data: unknown) => {
    if (typeof data === 'string') {
        console.log(`Recibido: ${data.toUpperCase()}`);
    }
};

eventos.on('mensaje', miCallback);
eventos.emit('mensaje', 'hola mundo'); // "Recibido: HOLA MUNDO"
eventos.off('mensaje', miCallback);

Mira la cantidad de conceptos que hemos usado: parámetros tipados, retorno void, callbacks con tipo arrow, unknown en vez de any para los datos, y type alias + interface para definir los contratos. Todo encaja perfecto porque es TypeScript profesional, nene.


7. Errores comunes con funciones

🚨 Usar Function como tipo

Es el any de las funciones. No valida nada. Usa la firma con arrow: (x: number) => void

⚠️ Olvidar tipar parámetros

Los parámetros NO se infieren. Con strict mode, TypeScript te obliga. Sin strict... tendrás any silencioso.

🔄 Confundir void con undefined

void significa "no me importa el retorno". undefined significa "devuelve exactamente undefined". Son distintos.

💡 No tipar callbacks recibidos

Si tu función recibe un callback, dale un tipo concreto. Así quien la llame tendrá autocompletado e inferencia dentro del callback.


8. Checklist: domina las funciones en TypeScript

0/10 — 0% blindado

Conclusión

Las funciones son el corazón de cualquier aplicación y tiparlas bien es lo que separa a un proyecto TypeScript profesional de uno que simplemente tiene archivos .ts. Recuerda lo importante:

En el próximo post veremos objetos, interfaces y type aliases: cómo definir la forma de tus datos, cuándo usar interface vs type, y patrones avanzados que te van a encantar.

EA, nos vemos en los bares 🍻


Pon a prueba lo aprendido

1. ¿Se infieren automáticamente los tipos de los parámetros de una función?

2. ¿Por qué es mala práctica usar el tipo Function para tipar callbacks?

3. ¿Cuál es la forma correcta de tipar un callback que recibe un string y no devuelve nada?

4. ¿Qué significa el tipo de retorno void?

5. ¿Cuándo una función tiene tipo de retorno never?

6. ¿Qué ventaja tiene escribir el tipo de retorno explícitamente?