En el capítulo 1 anotaste los parámetros y el retorno de una función, una a una. Eso describe una función concreta. Pero en TypeScript una función también es un valor, y como valor tiene su propio tipo. Saber escribir “el tipo de una función” es lo que te deja tipar una función que recibe otra función (un callback): un comparador para ordenar, un manejador de un evento (lo verás de lleno en React), una función que se pasa a map o filter.
Este capítulo cierra los tipos de función: cómo se escribe el tipo de una función, cómo flexibilizar sus parámetros (opcionales, por defecto, rest) y qué significa de verdad void.
El tipo de una función: la call signature#
El tipo de una función se escribe con una flecha: los parámetros entre paréntesis, y tras => el tipo que devuelve. A esa descripción se le llama call signature (firma de llamada):
// "Función que recibe dos Heroe y devuelve un number".
// No es una función: es el TIPO de una función con esa forma.
type Comparador = (a: Heroe, b: Heroe) => number;La diferencia con lo del capítulo 1 es importante. Allí anotabas una función concreta (function winrate(...): number). Aquí describes cualquier función que tenga esa forma, para poder tipar una variable o un parámetro que recibe una función:
// El segundo parámetro no es un dato: es OTRA función, el criterio de orden.
function rankearCon(lista: Heroe[], criterio: Comparador): Heroe[] {
return [...lista].sort(criterio);
}
// Al pasar la función, no hace falta anotar a y b: TypeScript los infiere
// como Heroe a partir del tipo Comparador del parámetro.
rankearCon(equipo, (a, b) => b.victorias - a.victorias);Ese es el gran porqué de las call signatures: cuando pasas un callback, TypeScript usa el tipo del parámetro para inferir los tipos de los argumentos del callback. Por eso, dentro de la flecha, a y b ya son Heroe sin que escribas nada, y devolver algo que no sea number da error antes de ejecutar.
Parámetros opcionales, por defecto y rest#
Hasta ahora todos los parámetros eran obligatorios y fijos. Hay tres formas de flexibilizar eso, y conviene no confundirlas.
Opcional con ?. Un parámetro marcado con ? puede pasarse o no. Si no se pasa, vale undefined, así que tienes que contemplarlo antes de usarlo (igual que las propiedades opcionales del capítulo 2):
// prefijo es opcional (con "?"): si no se pasa, vale undefined.
function etiquetar(nombre: string, prefijo?: string): string {
// Comprobamos antes de usarlo: si no vino, devolvemos el nombre a secas.
if (prefijo === undefined) {
return nombre;
}
return prefijo + nombre;
}Por defecto con = valor. En vez de dejarlo en undefined, le das un valor que se usa cuando se omite. TypeScript infiere el tipo del parámetro a partir de ese valor, y dentro de la función el parámetro siempre tiene un valor del tipo esperado (te ahorras la comprobación):
// Si no pasas tope, vale 3. No hace falta anotar ": number": lo infiere del 3.
function primeros(lista: string[], tope: number = 3): string[] {
return lista.slice(0, tope);
}Rest con .... Recoge todos los argumentos restantes en un array tipado. Es el mismo ... que ya viste en JavaScript, ahora con el tipo del array puesto. Va siempre el último:
// titulo es fijo; nombres recoge "de ahí en adelante" en un string[].
function alinear(titulo: string, ...nombres: string[]): string {
return titulo + ": " + nombres.join(" + ");
}
// alinear("Equipo", "Tracer", "Mercy") deja nombres = ["Tracer", "Mercy"].void: cuando una función no devuelve nada útil#
void es el tipo de retorno de una función que no devuelve nada útil: solo provoca un efecto (registrar por consola, guardar algo, disparar una acción) y no produce un valor que vayas a usar.
// No hay return con valor: la función solo registra. Su retorno es void.
function registrar(mensaje: string): void {
console.log("[LOG] " + mensaje);
}Hasta aquí, intuitivo. Pero void tiene un matiz que sorprende y conviene entender, porque lo vas a encontrar en cuanto uses callbacks como los de forEach. Cuando un tipo de función dice que el retorno es void, significa “el que llama no va a mirar lo que devuelvas”. Y por eso una función que sí devuelve un valor es asignable donde se espera un () => void:
const nombres: string[] = [];
// forEach espera un callback (valor) => void.
// push DEVUELVE un number (la nueva longitud), pero forEach lo ignora:
// esto es válido y NO da error.
equipo.forEach((n) => nombres.push(n));¿Y qué? Si pensaras que void significa “está prohibido devolver algo”, este código te parecería un error y no lo es. La lectura correcta es la contraria: void en una firma de callback es una promesa de que el resultado se ignora, no una prohibición. Saberlo te evita pelearte con el compilador cuando pasas a un forEach (o a un manejador de eventos de React, más adelante) una función de una línea que “devuelve” algo sin querer.
Comprueba lo que sabes#
Pregunta 1 de 4
¿Qué describe el tipo `(a: Heroe, b: Heroe) => number`?
Tu turno#
Tienes tres utilidades del Team Builder con tipos flojos: un callback como any, un parámetro que debería poder omitirse y una función sin su retorno anotado. Dales tipos honestos hasta que el panel “Problemas” quede vacío. Cuando lo tengas (o si te atascas), despliega las soluciones y fíjate en el salto de un nivel al siguiente.
Ejercicio · en esta página
Tipa las utilidades de función del Team Builder
Tienes tres funciones con tipos flojos: un callback como any, un parámetro que debería poder omitirse y una función sin su retorno anotado. Dales tipos honestos hasta que el panel "Problemas" quede vacío.
Paso 1: Que funcione
- Tipas el callback con su call signature inline: (a: Heroe, b: Heroe) => number.
- Le das a tope un valor por defecto (= 3) en lugar de exigirlo siempre.
- Anotas registrar con su retorno void. El panel queda vacío.
Paso 2: Que sea claro
- Extraes un alias type Comparador = (a: Heroe, b: Heroe) => number.
- Defines comparadores con nombre (porVictorias, porPartidas) de tipo Comparador.
- No repites la firma del comparador en cada sitio. Sin any.
Paso 3: Que sea potente
- Añades rankearPor(lista, ...criterios: Comparador[]) con parámetros rest.
- Acepta varios comparadores y los aplica en orden como desempate (victorias y, si empatan, partidas).
- Cada criterio está tipado como Comparador. Cero any en todo el fichero.
Ver soluciones
interface Heroe {
nombre: string;
rol: string;
partidas: number;
victorias: number;
}
const equipo: Heroe[] = [
{ nombre: "Tracer", rol: "Daño", partidas: 120, victorias: 78 },
{ nombre: "Mercy", rol: "Apoyo", partidas: 200, victorias: 155 },
{ nombre: "Reinhardt", rol: "Tanque", partidas: 95, victorias: 61 },
];
// criterio tipado con su call signature inline: una función (Heroe, Heroe) => number.
// Ahora el sort está protegido y a/b son Heroe dentro del callback.
function rankearCon(lista: Heroe[], criterio: (a: Heroe, b: Heroe) => number): Heroe[] {
return [...lista].sort(criterio);
}
// tope con valor por defecto: si no se pasa, vale 3.
function primeros(lista: Heroe[], tope: number = 3): Heroe[] {
return lista.slice(0, tope);
}
// retorno void explícito: la función no devuelve nada útil, solo registra.
function registrar(mensaje: string): void {
console.log("[LOG] " + mensaje);
}
const ranking = rankearCon(equipo, (a, b) => b.victorias - a.victorias);
const mejores = primeros(ranking);
mejores.forEach((h) => registrar(h.nombre + " — " + h.victorias + " victorias")); Por qué este nivel
- Tipa el callback inline con su call signature (a: Heroe, b: Heroe) => number: el sort queda protegido y, dentro, a y b ya son Heroe en vez de any.
- Le da a tope un valor por defecto (= 3): la función se puede llamar con o sin él, sin comprobar undefined a mano.
- Anota registrar como : void, dejando claro que no devuelve nada útil. Su límite: repite la firma del comparador allí donde haga falta.
interface Heroe {
nombre: string;
rol: string;
partidas: number;
victorias: number;
}
const equipo: Heroe[] = [
{ nombre: "Tracer", rol: "Daño", partidas: 120, victorias: 78 },
{ nombre: "Mercy", rol: "Apoyo", partidas: 200, victorias: 155 },
{ nombre: "Reinhardt", rol: "Tanque", partidas: 95, victorias: 61 },
];
// Un alias para el tipo de la función comparadora: se reutiliza y se lee mejor
// que repetir (a: Heroe, b: Heroe) => number en cada sitio.
type Comparador = (a: Heroe, b: Heroe) => number;
function rankearCon(lista: Heroe[], criterio: Comparador): Heroe[] {
return [...lista].sort(criterio);
}
// Comparadores con nombre: el código se documenta solo y son reutilizables.
const porVictorias: Comparador = (a, b) => b.victorias - a.victorias;
const porPartidas: Comparador = (a, b) => b.partidas - a.partidas;
// tope con valor por defecto.
function primeros(lista: Heroe[], tope: number = 3): Heroe[] {
return lista.slice(0, tope);
}
// retorno void explícito.
function registrar(mensaje: string): void {
console.log("[LOG] " + mensaje);
}
// Pasamos un comparador con nombre en vez de una flecha anónima.
const ranking = rankearCon(equipo, porVictorias);
primeros(ranking).forEach((h) => registrar(h.nombre + " — " + h.victorias + " victorias")); Por qué es mejor que el anterior
- Extrae type Comparador = (a: Heroe, b: Heroe) => number: una sola fuente de verdad para la forma del comparador, más legible que la firma inline repetida.
- Define comparadores con nombre (porVictorias, porPartidas) de tipo Comparador: el código se documenta solo y son reutilizables en varias llamadas.
- Mismo comportamiento que ok pero más limpio. El salto a excelente es hacer la API más potente, no solo más limpia.
interface Heroe {
nombre: string;
rol: string;
partidas: number;
victorias: number;
}
const equipo: Heroe[] = [
{ nombre: "Tracer", rol: "Daño", partidas: 120, victorias: 78 },
{ nombre: "Mercy", rol: "Apoyo", partidas: 200, victorias: 155 },
{ nombre: "Reinhardt", rol: "Tanque", partidas: 95, victorias: 61 },
];
// El tipo de la función comparadora, reutilizable en toda la API.
type Comparador = (a: Heroe, b: Heroe) => number;
// Comparadores con nombre.
const porVictorias: Comparador = (a, b) => b.victorias - a.victorias;
const porPartidas: Comparador = (a, b) => b.partidas - a.partidas;
// Parámetros rest: acepta CUALQUIER número de comparadores y los aplica en orden,
// usando los siguientes como desempate cuando el anterior da 0. Cada uno es un Comparador.
function rankearPor(lista: Heroe[], ...criterios: Comparador[]): Heroe[] {
return [...lista].sort((a, b) => {
// Probamos los criterios en orden: el primero que no empate decide.
for (const criterio of criterios) {
const resultado = criterio(a, b);
if (resultado !== 0) {
return resultado;
}
}
return 0;
});
}
// tope con valor por defecto.
function primeros(lista: Heroe[], tope: number = 3): Heroe[] {
return lista.slice(0, tope);
}
// retorno void explícito.
function registrar(mensaje: string): void {
console.log("[LOG] " + mensaje);
}
// Ordena por victorias y, en empate, por partidas: dos comparadores en una llamada.
const ranking = rankearPor(equipo, porVictorias, porPartidas);
primeros(ranking).forEach((h) => registrar(h.nombre + " — " + h.victorias + " victorias")); Por qué es mejor que el anterior
- rankearPor(lista, ...criterios: Comparador[]) usa parámetros rest: una sola función cubre "ordena por X" y "ordena por X y, en empate, por Y".
- Cada elemento del rest está tipado como Comparador: pasar algo que no cumpla esa forma es un error de compilación, no un fallo en ejecución.
- Ordenar por victorias y, en empate, por partidas queda en una llamada expresiva. Todo tipado, cero any: la API es más potente y más segura a la vez.