Saltar al contenido
funciones.ts · devschool

Funciones en TypeScript

Lección 7 de 15 · 12 min de lectura · Actualizado el

En esta lección
  1. Parámetros y retorno tipados
  2. Parámetros opcionales
  3. Valores por defecto
  4. Parámetros rest
  5. Parámetros con forma de objeto
  6. void: funciones que no devuelven nada
  7. never: funciones que no terminan
  8. El tipo de una función
  9. Sobrecargas
  10. Errores frecuentes
  11. Resumen

Las funciones son el lugar donde TypeScript más te ayuda: cada parámetro y cada valor devuelto es un contrato. Si alguien llama a la función con un dato equivocado, o si tú devuelves algo que no toca, el compilador lo avisa antes de ejecutar nada. En esta lección verás cómo tipar parámetros y retorno, cómo declarar parámetros opcionales, con valor por defecto y variables, qué significan void y never, cómo describir el tipo de una función para usarla como callback y cuándo conviene una sobrecarga.

Si necesitas repasar cómo funcionan las funciones en JavaScript, mira antes Funciones. Aquí solo añadimos los tipos.

Parámetros y retorno tipados

Los tipos de los parámetros se escriben después del nombre, y el tipo de lo que devuelve la función va después de los paréntesis:

function precioConIva(precio: number, iva: number): number {
  return precio * (1 + iva / 100);
}

console.log(precioConIva(50, 21)); // 60.5
  • precio: number y iva: number son los tipos de los parámetros.
  • : number tras el paréntesis es el tipo de retorno.

Con strict activado, un parámetro sin tipo es un error (noImplicitAny), porque TypeScript no puede adivinarlo:

function saludar(nombre) {
  return "Hola " + nombre;
}
// error TS7006: Parameter 'nombre' implicitly has an 'any' type.

Y TypeScript comprueba también cuántos argumentos pasas y de qué tipo:

precioConIva(50);
// error TS2554: Expected 2 arguments, but got 1.

precioConIva("50", 21);
// error TS2345: Argument of type 'string' is not assignable to parameter of type 'number'.

En JavaScript ambas llamadas se ejecutarían sin quejarse (y la segunda daría un resultado absurdo). Aquí no llegan ni a compilar.

¿Hay que escribir el tipo de retorno?

No siempre. Si no lo escribes, TypeScript lo infiere a partir de lo que devuelve la función:

function areaCirculo(radio: number) {
  return Math.PI * radio ** 2;
}
// TypeScript sabe que devuelve number

Entonces, ¿para qué escribirlo? Porque es una forma de decir qué quieres devolver. Si te equivocas dentro de la función, el error aparece donde está el fallo y no en quien la llama:

function nombreCompleto(nombre: string, apellido: string): string {
  if (apellido === "") {
    return;
  }
  return `${nombre} ${apellido}`;
}
// error TS2322: Type 'undefined' is not assignable to type 'string'.

Una buena regla: anota siempre el retorno en las funciones que exportas o que forman parte de la API de tu programa, y déjalo inferido en funciones pequeñas y locales, como las flechas que pasas a map.

Las funciones flecha se tipan igual:

const aCelsius = (fahrenheit: number): number => (fahrenheit - 32) / 1.8;

console.log(aCelsius(212)); // 100

Parámetros opcionales

Un parámetro marcado con ? se puede omitir. Dentro de la función su tipo incluye undefined, así que tienes que comprobarlo antes de usarlo:

function crearEtiqueta(nombre: string, cantidad?: number): string {
  if (cantidad === undefined) {
    return nombre;
  }
  return `${nombre} x${cantidad}`;
}

console.log(crearEtiqueta("Mosquetones"));    // Mosquetones
console.log(crearEtiqueta("Mosquetones", 6)); // Mosquetones x6

Aquí cantidad es number | undefined. Si intentaras hacer cantidad * 2 sin comprobar, TypeScript te avisaría con 'cantidad' is possibly 'undefined'. Comprobarlo con un if se llama estrechar el tipo y lo verás a fondo en la siguiente lección.

Cuidado: los parámetros opcionales deben ir después de los obligatorios. function f(a?: number, b: string) es un error, porque no habría forma de saltarse a al llamar.

Valores por defecto

Si en vez de undefined prefieres un valor concreto cuando el argumento falta, usa un valor por defecto:

function cuotaMensual(base: number, descuento: number = 0): number {
  return base - base * (descuento / 100);
}

console.log(cuotaMensual(40));     // 40
console.log(cuotaMensual(40, 25)); // 30

Con un valor por defecto, TypeScript infiere el tipo, así que normalmente puedes escribir solo descuento = 0. Y dentro de la función el parámetro ya es number, sin undefined: no tienes que comprobar nada.

function repetir(texto: string, veces = 2): string {
  return texto.repeat(veces);
}

console.log(repetir("ja"));    // jaja
console.log(repetir("ja", 3)); // jajaja

Un parámetro con valor por defecto también se puede omitir al llamar, igual que uno opcional. La diferencia es que ? te deja con un posible undefined y el valor por defecto te lo resuelve.

Parámetros rest

Para aceptar cualquier cantidad de argumentos se usa ... con un array:

function sumar(...numeros: number[]): number {
  let total = 0;
  for (const n of numeros) {
    total += n;
  }
  return total;
}

console.log(sumar(10, 20, 5)); // 35
console.log(sumar());          // 0

El parámetro rest (del inglés resto) recoge todos los argumentos sobrantes en un array, y su tipo siempre es un array. Solo puede haber uno y debe ser el último:

function registrar(grupo: string, ...asistentes: string[]): string {
  return `${grupo}: ${asistentes.length} asistentes`;
}

console.log(registrar("Yoga", "Ana", "Luis", "Marta")); // Yoga: 3 asistentes

También puedes expandir un array en una llamada con el operador spread, que tiene los mismos tres puntos:

const notas = [7, 8.5, 6];
console.log(sumar(...notas)); // 21.5

Nota: si una función espera un número fijo de argumentos y le pasas un array expandido, TypeScript se queja porque no puede saber cuántos elementos tiene. Con una tupla sí puede, porque su longitud es fija.

Parámetros con forma de objeto

Cuando una función tiene muchos parámetros, es fácil equivocar el orden: crearReserva("Ana", 2, true, false) no dice nada. Una solución muy usada es recibir un solo objeto y desestructurarlo, tipándolo con una interfaz o un alias:

interface OpcionesReserva {
  nombre: string;
  plazas?: number;
  seguro?: boolean;
}

function reservar({ nombre, plazas = 1, seguro = false }: OpcionesReserva): string {
  return `${nombre}: ${plazas} plaza(s)${seguro ? " con seguro" : ""}`;
}

console.log(reservar({ nombre: "Irene" }));                       // Irene: 1 plaza(s)
console.log(reservar({ nombre: "Pablo", plazas: 3, seguro: true })); // Pablo: 3 plaza(s) con seguro

El tipo va después de las llaves de la desestructuración, y los valores por defecto se escriben dentro. Al llamar, los nombres de las propiedades documentan cada dato, y no importa el orden.

void: funciones que no devuelven nada

Una función que hace algo (mostrar un mensaje, modificar una lista) pero no devuelve ningún valor tiene como retorno void:

function avisar(mensaje: string): void {
  console.log(`[aviso] ${mensaje}`);
}

avisar("Quedan 2 plazas"); // [aviso] Quedan 2 plazas

Si escribes return con un valor dentro de una función void, hay error. Y si no anotas el retorno, TypeScript infiere void solo.

never: funciones que no terminan

never es el retorno de una función que nunca vuelve al punto donde la llamaron: o lanza siempre un error, o es un bucle infinito.

function fallar(mensaje: string): never {
  throw new Error(mensaje);
}

function dividir(a: number, b: number): number {
  if (b === 0) {
    fallar("No se puede dividir entre cero");
  }
  return a / b;
}

console.log(dividir(9, 3)); // 3

No es lo mismo que void: una función void termina y devuelve el control; una never no. En Estrechar tipos verás otro uso de never: comprobar que cubres todos los casos de una unión.

El tipo de una función

En JavaScript las funciones son valores: se guardan en variables, se pasan a otras funciones y se devuelven. TypeScript tiene una sintaxis para describir la forma de una función, con flecha:

let operacion: (a: number, b: number) => number;

operacion = (x, y) => x + y;
console.log(operacion(4, 5)); // 9

operacion = (x, y) => `${x}${y}`;
// error TS2322: Type 'string' is not assignable to type 'number'.

Lee (a: number, b: number) => number como “una función que recibe dos números y devuelve un número”. Los nombres de los parámetros (a, b) solo sirven de documentación: no tienen que coincidir con los de la función real. Fíjate también en que al asignar la flecha no hay que anotar x ni y: TypeScript los deduce del tipo de operacion. Se llama tipado contextual.

Alias para tipos de función

Si vas a repetir el tipo, ponle nombre con type:

type Operacion = (a: number, b: number) => number;

const sumarDos: Operacion = (a, b) => a + b;
const multiplicar: Operacion = (a, b) => a * b;

console.log(sumarDos(3, 4), multiplicar(3, 4)); // 7 12

Callbacks

El uso más frecuente es recibir otra función como parámetro, un callback:

function aplicarATodos(lista: number[], fn: (n: number) => number): number[] {
  const resultado: number[] = [];
  for (const n of lista) {
    resultado.push(fn(n));
  }
  return resultado;
}

console.log(aplicarATodos([1, 2, 3], (n) => n * 10)); // [ 10, 20, 30 ]

Al pasar la flecha, n ya es number sin escribirlo. Si el callback devolviera un texto, TypeScript lo rechazaría.

Hay una regla que sorprende al principio: una función con menos parámetros es válida donde se espera una con más, porque ignorar argumentos no hace daño:

type Manejador = (nombre: string, indice: number) => void;

const imprimir: Manejador = (nombre) => console.log(nombre); // vale: no usa indice

Lo contrario, pedir más parámetros de los que se pasan, sí es un error.

Funciones que devuelven funciones

El retorno también puede ser una función. Se ve en patrones como los contadores o los descuentos:

function crearDescuento(porcentaje: number): (precio: number) => number {
  return (precio) => precio * (1 - porcentaje / 100);
}

const rebajas = crearDescuento(20);
console.log(rebajas(50)); // 40

Después de los dos puntos de crearDescuento va el tipo de la función devuelta: (precio: number) => number. Si te resulta difícil de leer, un alias lo aclara: type Calculo = (precio: number) => number.

Sobrecargas

A veces una función admite varias formas de llamarla con resultados distintos. Una sobrecarga (overload) declara cada forma permitida y después hay una única implementación:

function obtener(id: number): string;
function obtener(ids: number[]): string[];
function obtener(entrada: number | number[]): string | string[] {
  if (typeof entrada === "number") {
    return `Socio ${entrada}`;
  }
  return entrada.map((id) => `Socio ${id}`);
}

const uno = obtener(7);         // string
const varios = obtener([1, 2]); // string[]
console.log(uno);    // Socio 7
console.log(varios); // [ 'Socio 1', 'Socio 2' ]

Las dos primeras líneas son las firmas que verá quien use la función, y la tercera es la implementación, que debe ser compatible con todas pero no es visible desde fuera. Gracias a ello uno es string y varios es string[], no una unión que haya que estrechar. Si llamas con otra cosa, como obtener("7"), da error.

Consejo: usa sobrecargas solo cuando el tipo de retorno depende de cómo se llama. Si únicamente cambia el tipo del parámetro y el resultado es el mismo, basta con una unión: function mostrar(valor: string | number): string. Es más sencillo y suele ser suficiente.

Errores frecuentes

  • Olvidar el tipo de un parámetro: con strict es el error implicitly has an 'any' type. Anota siempre los parámetros de las funciones con nombre.
  • Poner un opcional antes de un obligatorio: los ? van al final de la lista.
  • Usar un parámetro opcional sin comprobarlo: cantidad?: number es number | undefined. Comprueba con if o dale un valor por defecto.
  • Confundir void con never: void devuelve el control sin valor; never no vuelve jamás (lanza error o no termina).
  • Escribir : void y devolver algo: el compilador lo rechaza. Si la función sí devuelve algo, cambia el tipo.
  • Sobrecargar cuando basta una unión: más firmas, más código y el mismo resultado.
  • Confundir la flecha del tipo con la de la función: en (a: number) => number la flecha pertenece al tipo; la flecha de una función real lleva cuerpo detrás: (a: number): number => a * 2.

Resumen

ConceptoEjemplo
Parámetros y retornofunction f(a: number, b: number): number
Flecha tipadaconst f = (a: number): number => a * 2
Opcionalfunction f(a: string, b?: number) (b es number | undefined)
Valor por defectofunction f(a: number, iva = 21)
Restfunction f(...nums: number[])
Objeto de opcionesfunction f({ nombre, plazas = 1 }: Opciones)
Sin valor devueltofunction f(): void
Nunca terminafunction f(): never { throw new Error("x") }
Tipo de función(a: number, b: number) => number
Alias de funcióntype Operacion = (a: number, b: number) => number
SobrecargaVarias firmas y una implementación compatible

En la siguiente lección aprenderás a estrechar tipos para decidir qué hacer según el tipo real de un valor.

Pon a prueba lo que has aprendido

[TypeScript] ¿Qué error da la llamada?
function precioConIva(precio: number, iva: number): number {
  return precio * (1 + iva / 100);
}
precioConIva(50);

[TypeScript] En function f(a: number, iva = 21), ¿qué tipo tiene iva dentro de la función?

[TypeScript] ¿Cuál es la diferencia entre void y never como tipo de retorno?

[TypeScript] ¿Es válido este código?
type Manejador = (nombre: string, indice: number) => void;
const imprimir: Manejador = (nombre) => console.log(nombre);

¿Te ha quedado claro? Márcala y verás tu progreso en el explorador.