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.

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 💡 strict El modo estricto de TypeScript (strict: true en tsconfig.json). Activa múltiples comprobaciones como noImplicitAny, strictNullChecks y más. Es la configuración recomendada.
Más info →
, 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.
El tipo es string | undefined. Puede no venir y necesitas comprobar antes de usar.
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:
- 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
- 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 arrow functions Funciones flecha de ES6+. Sintaxis más corta: (params) => expresión. También capturan el contexto this del scope padre. . 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 💡 callback Función que se pasa como argumento a otra función para que sea ejecutada más tarde. Patrón fundamental en JavaScript para manejar operaciones asíncronas y eventos. Más info → ), 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ística | Function (❌) | (params) => return (✅) |
|---|---|---|
| Valida parámetros | No | Sí, tipo y cantidad |
| Valida retorno | No | Sí, tipo exacto |
| Autocompletado | Ninguno | Completo dentro del callback |
| Inferencia en params | No | Sí, automática |
| Seguridad | Nula (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:
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...
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 💡 exhaustive checking Patrón en TypeScript donde se usa el tipo never en el default de un switch para asegurarse de que todos los casos posibles de un tipo unión están cubiertos. Si añades un nuevo caso al tipo y no lo manejas, el compilador da error.
Más info →
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
| Concepto | void | never |
|---|---|---|
| ¿La función termina? | Sí, normalmente | No, NUNCA |
| ¿Tiene return? | No, o return vacío | Imposible (throw o bucle ∞) |
| Ejemplo típico | console.log, guardar datos | throw Error, while(true) |
| ¿Se puede usar el resultado? | No (es undefined) | No existe resultado |
| ¿Cuándo usarlo? | Funciones que hacen algo pero no devuelven | Funciones 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
Es el any de las funciones. No valida nada. Usa la firma con arrow: (x: number) => void
Los parámetros NO se infieren. Con strict mode, TypeScript te obliga. Sin strict... tendrás any silencioso.
void significa "no me importa el retorno". undefined significa "devuelve exactamente undefined". Son distintos.
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
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:
- Los parámetros no se infieren, siempre típalos
- El retorno se infiere, pero a veces merece la pena escribirlo para documentar y proteger
- Los callbacks se tipan con arrow function syntax, nunca con
Function void= la función termina pero no devuelve nada.never= la función nunca termina
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?