TypeScript

TypeScript

Tutorial TypeScript: Funciones de primera clase y orden superior

Aprende a usar funciones como valores y funciones de orden superior en TypeScript para escribir código modular y reutilizable con tipado seguro.

Aprende TypeScript y certifícate

Funciones como valores en TypeScript

En TypeScript, las funciones son tratadas como ciudadanos de primera clase, lo que significa que pueden ser manipuladas como cualquier otro valor. Esta característica fundamental de la programación funcional nos permite trabajar con funciones de manera flexible y expresiva.

Asignación de funciones a variables

La forma más básica de tratar funciones como valores es asignarlas a variables. En TypeScript, podemos almacenar tanto funciones con nombre como funciones anónimas en variables:

// Función con nombre asignada a una variable
function greet(name: string): string {
  return `Hello, ${name}!`;
}

const sayHello = greet;
console.log(sayHello("Alice")); // Output: Hello, Alice!

// Función anónima asignada directamente a una variable
const add = function(a: number, b: number): number {
  return a + b;
};

console.log(add(5, 3)); // Output: 8

En este ejemplo, sayHello se convierte en una referencia a la función greet, no una copia. Cualquier llamada a sayHello ejecutará el código de la función original.

Funciones flecha como valores

Las funciones flecha proporcionan una sintaxis más concisa para asignar funciones a variables:

// Función flecha con bloque de código
const multiply = (a: number, b: number): number => {
  return a * b;
};

// Función flecha con retorno implícito (sin llaves)
const divide = (a: number, b: number): number => a / b;

console.log(multiply(4, 5)); // Output: 20
console.log(divide(10, 2));  // Output: 5

Tipado de funciones como valores

TypeScript nos permite definir tipos para nuestras funciones cuando las tratamos como valores, lo que mejora la seguridad de tipos:

// Definición de un tipo de función
type MathOperation = (x: number, y: number) => number;

// Variables con tipo de función explícito
const sum: MathOperation = (a, b) => a + b;
const subtract: MathOperation = (a, b) => a - b;

// Uso de las funciones tipadas
console.log(sum(10, 5));      // Output: 15
console.log(subtract(10, 5)); // Output: 5

El tipo MathOperation define una firma de función que acepta dos números y devuelve un número. Esto nos ayuda a garantizar que todas las funciones que asignemos a variables de este tipo cumplan con esta estructura.

Almacenamiento de funciones en estructuras de datos

Al ser valores, las funciones pueden almacenarse en arrays, objetos y otras estructuras de datos:

// Array de funciones
const mathOperations: MathOperation[] = [
  (a, b) => a + b,
  (a, b) => a - b,
  (a, b) => a * b,
  (a, b) => a / b
];

// Ejecutar la tercera función del array (multiplicación)
console.log(mathOperations[2](6, 3)); // Output: 18

// Objeto con funciones como propiedades
const calculator = {
  add: (a: number, b: number): number => a + b,
  subtract: (a: number, b: number): number => a - b,
  multiply: (a: number, b: number): number => a * b,
  divide: (a: number, b: number): number => a / b
};

console.log(calculator.multiply(4, 2)); // Output: 8

Paso de funciones como argumentos

Una aplicación práctica de las funciones como valores es pasarlas como argumentos a otras funciones:

// Función que acepta otra función como argumento
function applyOperation(a: number, b: number, operation: MathOperation): number {
  return operation(a, b);
}

// Pasamos diferentes funciones como argumentos
console.log(applyOperation(10, 5, (x, y) => x + y)); // Output: 15
console.log(applyOperation(10, 5, (x, y) => x * y)); // Output: 50

Este ejemplo muestra cómo podemos parametrizar el comportamiento de una función pasando otra función como argumento, lo que nos permite reutilizar código de manera efectiva.

Aplicación práctica: Transformación de datos

Un caso de uso común para funciones como valores es la transformación de datos:

// Definimos un tipo para nuestros transformadores
type Transformer<T> = (data: T) => T;

// Función que aplica una transformación a un valor
function transform<T>(value: T, transformer: Transformer<T>): T {
  return transformer(value);
}

// Diferentes transformadores para strings
const toUpperCase: Transformer<string> = (text) => text.toUpperCase();
const addExclamation: Transformer<string> = (text) => `${text}!`;
const reverse: Transformer<string> = (text) => text.split('').reverse().join('');

// Aplicamos las transformaciones
const message = "hello world";
console.log(transform(message, toUpperCase));     // Output: HELLO WORLD
console.log(transform(message, addExclamation));  // Output: hello world!
console.log(transform(message, reverse));         // Output: dlrow olleh

Este patrón es extremadamente flexible y nos permite crear sistemas de procesamiento de datos modulares y extensibles.

Funciones anónimas en línea

También podemos crear y usar funciones como valores directamente en línea, sin asignarlas a variables:

// Array de números
const numbers = [1, 2, 3, 4, 5];

// Uso de función anónima en línea con el método map
const doubled = numbers.map(function(num) {
  return num * 2;
});

// Versión más concisa con función flecha
const tripled = numbers.map(num => num * 3);

console.log(doubled);  // Output: [2, 4, 6, 8, 10]
console.log(tripled);  // Output: [3, 6, 9, 12, 15]

En este ejemplo, pasamos funciones anónimas directamente al método map sin necesidad de definirlas previamente, lo que hace que nuestro código sea más conciso y expresivo.

Funciones que reciben funciones como parámetros

Las funciones de orden superior son aquellas que pueden recibir otras funciones como parámetros. Esta característica es fundamental en la programación funcional y permite crear código más modular, reutilizable y expresivo en TypeScript.

Concepto básico

Una función de orden superior es simplemente una función que acepta otra función como argumento. Esto nos permite parametrizar el comportamiento de nuestras funciones, haciendo que sean más flexibles:

// Función de orden superior básica
function executeOperation(operation: (x: number) => number, value: number): number {
  return operation(value);
}

// Funciones que podemos pasar como parámetros
const square = (x: number): number => x * x;
const double = (x: number): number => x * 2;

// Uso de la función de orden superior
console.log(executeOperation(square, 5)); // Output: 25
console.log(executeOperation(double, 5)); // Output: 10

En este ejemplo, executeOperation es una función de orden superior que acepta otra función como su primer parámetro.

Tipado de funciones como parámetros

TypeScript nos permite definir con precisión los tipos de las funciones que recibimos como parámetros:

// Usando tipos de función explícitos
function processArray<T, U>(
  array: T[], 
  processor: (item: T, index: number) => U
): U[] {
  const result: U[] = [];
  for (let i = 0; i < array.length; i++) {
    result.push(processor(array[i], i));
  }
  return result;
}

const numbers = [1, 2, 3, 4, 5];

// Pasamos una función que convierte números a strings
const stringified = processArray(numbers, (num, index) => 
  `Item ${index}: ${num}`
);

console.log(stringified);
// Output: ["Item 0: 1", "Item 1: 2", "Item 2: 3", "Item 3: 4", "Item 4: 5"]

El tipo (item: T, index: number) => U define la firma de la función que processArray espera recibir.

Callbacks: un caso común de funciones como parámetros

Los callbacks son un patrón común donde pasamos una función que será llamada cuando ocurra algún evento o se complete una operación:

// Función que simula una operación asíncrona
function fetchData(
  resource: string, 
  onSuccess: (data: any) => void, 
  onError: (error: Error) => void
): void {
  try {
    // Simulamos obtener datos
    const data = { id: 1, name: "Example Data" };
    // Llamamos al callback de éxito
    onSuccess(data);
  } catch (error) {
    // Llamamos al callback de error
    onError(error as Error);
  }
}

// Uso con callbacks
fetchData(
  "users",
  (data) => console.log("Data received:", data),
  (error) => console.error("Error:", error.message)
);

Este patrón es especialmente útil para operaciones asíncronas, aunque en código moderno se prefieren las Promesas y async/await.

Funciones de orden superior para manipulación de arrays

TypeScript incluye métodos nativos para arrays que son funciones de orden superior, como map, filter, reduce, forEach, etc:

const products = [
  { id: 1, name: "Laptop", price: 1200, inStock: true },
  { id: 2, name: "Phone", price: 800, inStock: true },
  { id: 3, name: "Tablet", price: 500, inStock: false },
  { id: 4, name: "Headphones", price: 150, inStock: true }
];

// filter: recibe una función que determina si incluir cada elemento
const availableProducts = products.filter(product => product.inStock);

// map: recibe una función que transforma cada elemento
const productNames = products.map(product => product.name);

// find: recibe una función que busca un elemento específico
const expensiveProduct = products.find(product => product.price > 1000);

// some: recibe una función que verifica si al menos un elemento cumple una condición
const hasAffordableOptions = products.some(product => product.price < 200);

console.log("Available products:", availableProducts.length); // Output: 3
console.log("Product names:", productNames); // Output: ["Laptop", "Phone", "Tablet", "Headphones"]
console.log("Expensive product:", expensiveProduct?.name); // Output: Laptop
console.log("Has affordable options:", hasAffordableOptions); // Output: true

Estos métodos nos permiten escribir código más declarativo y conciso.

Implementación de patrones funcionales

Las funciones de orden superior nos permiten implementar patrones funcionales comunes:

// Patrón: Función decoradora
function withLogging<T extends any[], R>(
  fn: (...args: T) => R
): (...args: T) => R {
  return function(...args: T): R {
    console.log(`Calling function with arguments: ${args}`);
    const result = fn(...args);
    console.log(`Function returned: ${result}`);
    return result;
  };
}

// Función original
function multiply(a: number, b: number): number {
  return a * b;
}

// Función decorada con logging
const multiplyWithLogging = withLogging(multiply);

// Uso
const result = multiplyWithLogging(5, 3);
// Output:
// Calling function with arguments: 5,3
// Function returned: 15

En este ejemplo, withLogging es una función de orden superior que extiende el comportamiento de cualquier función que reciba, añadiendo logs antes y después de su ejecución.

Estrategias de validación con funciones como parámetros

Podemos usar funciones como parámetros para implementar estrategias de validación flexibles:

type Validator<T> = (value: T) => boolean;

function validate<T>(value: T, validators: Validator<T>[]): boolean {
  return validators.every(validator => validator(value));
}

// Validadores para strings
const isNotEmpty = (text: string): boolean => text.trim().length > 0;
const isEmail = (text: string): boolean => /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(text);
const hasMinLength = (minLength: number): Validator<string> => 
  (text: string) => text.length >= minLength;

// Validación de un email
const email = "user@example.com";
const isValidEmail = validate(email, [
  isNotEmpty,
  isEmail,
  hasMinLength(5)
]);

console.log(`Is ${email} valid?`, isValidEmail); // Output: Is user@example.com valid? true

Este patrón nos permite componer validaciones de manera flexible y reutilizable.

Funciones de orden superior para manejo de eventos

En aplicaciones web, las funciones de orden superior son útiles para el manejo de eventos:

type EventHandler = (event: Event) => void;

function createButtonWithHandler(
  text: string, 
  handler: EventHandler
): HTMLButtonElement {
  const button = document.createElement("button");
  button.textContent = text;
  button.addEventListener("click", handler);
  return button;
}

// Uso
const saveButton = createButtonWithHandler("Save", (event) => {
  console.log("Save button clicked", event);
  // Lógica para guardar datos
});

const cancelButton = createButtonWithHandler("Cancel", (event) => {
  console.log("Cancel button clicked", event);
  // Lógica para cancelar operación
});

// Añadir botones al DOM
document.body.appendChild(saveButton);
document.body.appendChild(cancelButton);

Este enfoque nos permite encapsular la creación de elementos y su comportamiento en funciones reutilizables.

Funciones de orden superior para control de flujo

Podemos usar funciones de orden superior para implementar patrones de control de flujo personalizados:

// Función para reintentar operaciones
function retry<T>(
  operation: () => Promise<T>, 
  maxAttempts: number, 
  delay: number
): Promise<T> {
  return new Promise<T>(async (resolve, reject) => {
    let attempts = 0;
    
    while (attempts < maxAttempts) {
      try {
        const result = await operation();
        return resolve(result);
      } catch (error) {
        attempts++;
        console.log(`Attempt ${attempts} failed. Retrying...`);
        
        if (attempts >= maxAttempts) {
          return reject(new Error(`Operation failed after ${maxAttempts} attempts`));
        }
        
        // Esperar antes del siguiente intento
        await new Promise(r => setTimeout(r, delay));
      }
    }
  });
}

// Uso
async function fetchUserData(userId: string): Promise<any> {
  // Simulación de una API que puede fallar
  const random = Math.random();
  if (random < 0.7) {
    throw new Error("Network error");
  }
  return { id: userId, name: "John Doe" };
}

// Intentar la operación con reintentos
retry(
  () => fetchUserData("user123"),
  3,  // máximo 3 intentos
  1000 // 1 segundo entre intentos
)
  .then(data => console.log("Success:", data))
  .catch(error => console.error("Final error:", error.message));

Este patrón es especialmente útil para operaciones que pueden fallar temporalmente, como peticiones de red.

Funciones que retornan funciones

En TypeScript, las funciones pueden no solo recibir otras funciones como parámetros, sino también retornar funciones como resultado. Esta capacidad es una característica fundamental de la programación funcional que permite crear código más modular, reutilizable y expresivo.

Concepto básico

Una función que retorna otra función crea una clausura (closure), que es una función interna que tiene acceso a las variables de su función contenedora, incluso después de que esta haya terminado de ejecutarse:

// Función que retorna otra función
function createMultiplier(factor: number): (value: number) => number {
  // Retorna una función que usa el parámetro 'factor'
  return function(value: number): number {
    return value * factor;
  };
}

// Creamos funciones específicas
const double = createMultiplier(2);
const triple = createMultiplier(3);

console.log(double(5)); // Output: 10
console.log(triple(5)); // Output: 15

En este ejemplo, createMultiplier es una fábrica de funciones que genera multiplicadores personalizados según el factor proporcionado.

Tipado de funciones retornadas

TypeScript nos permite definir con precisión los tipos de las funciones que retornamos:

// Usando tipos explícitos para la función retornada
function createFormatter(prefix: string): (value: string) => string {
  return (value: string): string => `${prefix}: ${value}`;
}

// Usando type alias para mayor claridad
type StringTransformer = (value: string) => string;

function createPrefixer(prefix: string): StringTransformer {
  return (text: string): string => `${prefix}${text}`;
}

// Uso de las funciones
const logFormatter = createFormatter("LOG");
const errorFormatter = createFormatter("ERROR");

const addHttps = createPrefixer("https://");

console.log(logFormatter("Operation completed")); // Output: LOG: Operation completed
console.log(errorFormatter("Connection failed")); // Output: ERROR: Connection failed
console.log(addHttps("example.com")); // Output: https://example.com

El tipado explícito mejora la seguridad y la legibilidad del código.

Currying: transformación de funciones

El currying es una técnica donde transformamos una función que toma múltiples argumentos en una secuencia de funciones que toman un solo argumento cada una:

// Versión normal de una función con múltiples parámetros
function add(a: number, b: number): number {
  return a + b;
}

// Versión currificada
function curriedAdd(a: number): (b: number) => number {
  return function(b: number): number {
    return a + b;
  };
}

// Versión con arrow functions (más concisa)
const curriedAddArrow = (a: number) => (b: number): number => a + b;

// Uso
console.log(add(2, 3)); // Output: 5

const addTwo = curriedAdd(2);
console.log(addTwo(3)); // Output: 5

// También podemos llamarla directamente
console.log(curriedAddArrow(2)(3)); // Output: 5

El currying permite la aplicación parcial de funciones, lo que facilita la creación de funciones especializadas a partir de funciones más generales.

Implementación de un helper de currying genérico

Podemos crear una función de utilidad para currificar automáticamente cualquier función:

// Helper de currying para funciones de dos parámetros
function curry<T, U, R>(fn: (a: T, b: U) => R): (a: T) => (b: U) => R {
  return (a: T) => (b: U) => fn(a, b);
}

// Funciones originales
function multiply(a: number, b: number): number {
  return a * b;
}

function concatenate(a: string, b: string): string {
  return a + b;
}

// Versiones currificadas
const curriedMultiply = curry(multiply);
const curriedConcatenate = curry(concatenate);

// Uso
const multiplyBy5 = curriedMultiply(5);
console.log(multiplyBy5(3)); // Output: 15

const addHello = curriedConcatenate("Hello, ");
console.log(addHello("world")); // Output: Hello, world

Esta técnica es especialmente útil para componer funciones y crear pipelines de procesamiento de datos.

Creación de configuradores con funciones que retornan funciones

Las funciones que retornan funciones son ideales para crear configuradores que permiten personalizar comportamientos:

// Configurador para peticiones HTTP
type HttpMethod = 'GET' | 'POST' | 'PUT' | 'DELETE';
type RequestConfig = {
  headers?: Record<string, string>;
  timeout?: number;
};

function createApiClient(baseUrl: string) {
  return function(method: HttpMethod) {
    return function(endpoint: string, config?: RequestConfig) {
      return async function<T>(data?: any): Promise<T> {
        const url = `${baseUrl}${endpoint}`;
        console.log(`Making ${method} request to ${url}`);
        
        // Aquí iría la implementación real con fetch o axios
        // Este es solo un ejemplo simplificado
        return {} as T;
      };
    };
  };
}

// Uso del configurador
const apiClient = createApiClient('https://api.example.com');
const get = apiClient('GET');
const post = apiClient('POST');

// Endpoints específicos
const getUsers = get('/users', { timeout: 5000 });
const createUser = post('/users', { 
  headers: { 'Content-Type': 'application/json' } 
});

// Uso
async function fetchData() {
  const users = await getUsers<User[]>();
  const newUser = await createUser<User>({ name: 'John', email: 'john@example.com' });
}

Este patrón permite crear APIs fluidas con una excelente experiencia de desarrollo.

Memoización con funciones que retornan funciones

La memoización es una técnica de optimización que almacena los resultados de llamadas a funciones costosas y devuelve el resultado en caché cuando se realizan las mismas llamadas:

// Función de memoización genérica
function memoize<T, R>(fn: (arg: T) => R): (arg: T) => R {
  const cache = new Map<T, R>();
  
  return function(arg: T): R {
    if (cache.has(arg)) {
      console.log(`Cache hit for argument: ${String(arg)}`);
      return cache.get(arg)!;
    }
    
    console.log(`Computing result for: ${String(arg)}`);
    const result = fn(arg);
    cache.set(arg, result);
    return result;
  };
}

// Función costosa (ejemplo: cálculo de Fibonacci)
function fibonacci(n: number): number {
  if (n <= 1) return n;
  return fibonacci(n - 1) + fibonacci(n - 2);
}

// Versión memoizada
const memoizedFibonacci = memoize(fibonacci);

// Uso
console.log(memoizedFibonacci(40)); // Primera llamada: calcula
console.log(memoizedFibonacci(40)); // Segunda llamada: usa caché

La memoización es especialmente útil para funciones puras con cálculos intensivos que se llaman repetidamente con los mismos argumentos.

Composición de funciones

La composición es una técnica donde combinamos múltiples funciones para crear una nueva función. Podemos implementarla usando funciones que retornan funciones:

// Función de composición
function compose<T, U, V>(
  f: (x: U) => V,
  g: (x: T) => U
): (x: T) => V {
  return (x: T) => f(g(x));
}

// Funciones para componer
const addOne = (x: number): number => x + 1;
const double = (x: number): number => x * 2;
const toString = (x: number): string => `Result: ${x}`;

// Componemos las funciones
const addOneThenDouble = compose(double, addOne);
const doubleThenToString = compose(toString, double);
const addOneDoubleThenToString = compose(toString, addOneThenDouble);

// Uso
console.log(addOneThenDouble(3)); // Output: 8 (3+1, luego *2)
console.log(doubleThenToString(3)); // Output: "Result: 6"
console.log(addOneDoubleThenToString(3)); // Output: "Result: 8"

La composición nos permite construir pipelines de procesamiento de datos de manera declarativa y modular.

Creación de middlewares

Los middlewares son un patrón común en aplicaciones web donde cada middleware procesa una solicitud y decide si pasarla al siguiente middleware:

type Request = { path: string; body: any };
type Response = { status: number; body: any };
type NextFunction = () => Promise<void>;
type Middleware = (req: Request, res: Response, next: NextFunction) => Promise<void>;

function createMiddlewareChain(middlewares: Middleware[]) {
  return async function(req: Request, res: Response): Promise<void> {
    let index = 0;
    
    async function executeNext(): Promise<void> {
      // Si hemos llegado al final de la cadena, terminamos
      if (index >= middlewares.length) return;
      
      // Obtenemos el middleware actual e incrementamos el índice
      const current = middlewares[index++];
      
      // Ejecutamos el middleware actual con la función next
      await current(req, res, executeNext);
    }
    
    // Iniciamos la cadena
    await executeNext();
  };
}

// Ejemplo de middlewares
const loggerMiddleware: Middleware = async (req, res, next) => {
  console.log(`Request to ${req.path}`);
  await next();
};

const authMiddleware: Middleware = async (req, res, next) => {
  if (req.path.startsWith('/admin')) {
    res.status = 401;
    res.body = { error: 'Unauthorized' };
    return; // No llamamos a next(), terminando la cadena
  }
  await next();
};

// Creamos y usamos la cadena
const handleRequest = createMiddlewareChain([
  loggerMiddleware,
  authMiddleware
]);

// Simulación de uso
async function simulateRequest() {
  const req = { path: '/users', body: {} };
  const res = { status: 200, body: null };
  
  await handleRequest(req, res);
  console.log(`Response: ${res.status}`, res.body);
}

simulateRequest();

Este patrón es la base de frameworks como Express.js y permite una separación clara de responsabilidades.

Aplicación práctica: Creación de validadores encadenables

Podemos usar funciones que retornan funciones para crear validadores encadenables:

// Definimos un tipo para nuestro validador
type Validator<T> = {
  validate: (value: T) => boolean;
  and: (nextValidator: Validator<T>) => Validator<T>;
  or: (nextValidator: Validator<T>) => Validator<T>;
};

// Función para crear validadores
function createValidator<T>(validateFn: (value: T) => boolean): Validator<T> {
  const validator: Validator<T> = {
    validate: validateFn,
    
    and: function(nextValidator: Validator<T>): Validator<T> {
      return createValidator<T>((value: T) => 
        this.validate(value) && nextValidator.validate(value)
      );
    },
    
    or: function(nextValidator: Validator<T>): Validator<T> {
      return createValidator<T>((value: T) => 
        this.validate(value) || nextValidator.validate(value)
      );
    }
  };
  
  return validator;
}

// Creamos validadores básicos
const isNotEmpty = createValidator<string>(value => value.trim().length > 0);
const isEmail = createValidator<string>(value => 
  /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value)
);
const hasMinLength = (min: number) => createValidator<string>(
  value => value.length >= min
);

// Componemos validadores
const isValidEmail = isNotEmpty
  .and(isEmail)
  .and(hasMinLength(5));

// Uso
const email1 = "user@example.com";
const email2 = "";
const email3 = "invalid";

console.log(`"${email1}" is valid:`, isValidEmail.validate(email1)); // true
console.log(`"${email2}" is valid:`, isValidEmail.validate(email2)); // false
console.log(`"${email3}" is valid:`, isValidEmail.validate(email3)); // false

Este patrón permite crear validaciones complejas a partir de validaciones simples de manera legible y mantenible.

Tipado correcto de funciones de orden superior

El tipado de funciones de orden superior en TypeScript es fundamental para garantizar la seguridad de tipos y aprovechar al máximo las capacidades del sistema de tipos. Un tipado correcto nos permite detectar errores en tiempo de compilación y mejorar la experiencia de desarrollo.

Sintaxis básica para tipar funciones

Antes de profundizar en funciones de orden superior, repasemos la sintaxis básica para tipar funciones en TypeScript:

// Usando type para definir un tipo de función
type Calculator = (a: number, b: number) => number;

// Usando interface (alternativa)
interface Comparator {
  (a: string, b: string): boolean;
}

// Declaración de función con tipo explícito
const sum: Calculator = (a, b) => a + b;
const areEqual: Comparator = (a, b) => a === b;

Tipado de funciones que reciben funciones

Cuando una función recibe otra función como parámetro, debemos definir claramente la firma de esa función parámetro:

// Función que recibe otra función como parámetro
function executeWithLogging<T, R>(
  input: T,
  processor: (data: T) => R
): R {
  console.log(`Processing input: ${input}`);
  const result = processor(input);
  console.log(`Result: ${result}`);
  return result;
}

// Uso
const numberToString = (num: number): string => num.toString();
const result = executeWithLogging(42, numberToString);

En este ejemplo, el parámetro processor está tipado como una función que recibe un dato de tipo T y devuelve un valor de tipo R.

Tipado de funciones que retornan funciones

Para funciones que retornan otras funciones, debemos especificar tanto el tipo de la función principal como el de la función retornada:

// Función que retorna otra función
function createFormatter<T>(prefix: string): (value: T) => string {
  return (value: T): string => `${prefix}: ${value}`;
}

// Uso con inferencia de tipos
const logFormatter = createFormatter<number>("LOG");
console.log(logFormatter(42)); // Output: LOG: 42

// También podemos usar type alias para mayor claridad
type Formatter<T> = (value: T) => string;

function createPrefixer<T>(prefix: string): Formatter<T> {
  return (value: T) => `${prefix}${value}`;
}

Uso de genéricos para mayor flexibilidad

Los tipos genéricos son esenciales para crear funciones de orden superior reutilizables:

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

const numbers = [1, 2, 3, 4, 5];
const doubled = map(numbers, n => n * 2);
const asStrings = map(numbers, n => n.toString());

// TypeScript infiere correctamente los tipos:
// doubled: number[]
// asStrings: string[]

Los genéricos nos permiten crear funciones que funcionan con cualquier tipo mientras mantienen la seguridad de tipos.

Tipado de callbacks con parámetros opcionales

Cuando trabajamos con callbacks que tienen parámetros opcionales, debemos ser precisos en nuestro tipado:

// Callback con parámetros opcionales
type FetchCallback<T> = (data: T, meta?: { timestamp: number }) => void;

function fetchData<T>(
  url: string, 
  callback: FetchCallback<T>
): void {
  // Simulación de fetch
  const data = { id: 1, name: "Example" } as unknown as T;
  const meta = { timestamp: Date.now() };
  
  // Llamamos al callback con o sin metadatos
  const includeMetadata = Math.random() > 0.5;
  if (includeMetadata) {
    callback(data, meta);
  } else {
    callback(data);
  }
}

// Uso
fetchData<{ id: number, name: string }>(
  "https://api.example.com/data",
  (data, meta) => {
    console.log(`Received: ${data.name}`);
    if (meta) {
      console.log(`Timestamp: ${meta.timestamp}`);
    }
  }
);

Tipado de funciones con this contextual

Cuando necesitamos preservar el contexto this en nuestras funciones, TypeScript nos permite especificarlo:

// Definición de tipo con contexto this
interface EventHandler {
  (this: HTMLElement, ev: Event): void;
}

// Función que recibe un handler con contexto this
function addClickHandler(element: HTMLElement, handler: EventHandler): void {
  element.addEventListener("click", handler);
}

// Uso (debe ser una función normal, no arrow function)
const button = document.createElement("button");
addClickHandler(button, function(this: HTMLElement, ev) {
  console.log(`Button ${this.id} was clicked at ${ev.timeStamp}`);
});

Este patrón es especialmente útil cuando trabajamos con APIs del DOM o código heredado.

Tipado de funciones con número variable de argumentos

Para funciones que aceptan un número variable de argumentos, usamos el operador rest:

// Función que recibe una función con número variable de argumentos
function applyOperation<T>(
  operation: (...args: T[]) => T,
  ...values: T[]
): T {
  return operation(...values);
}

// Funciones que podemos pasar
const sum = (...numbers: number[]): number => 
  numbers.reduce((total, n) => total + n, 0);

const concat = (...strings: string[]): string => 
  strings.join("");

// Uso
console.log(applyOperation(sum, 1, 2, 3, 4)); // Output: 10
console.log(applyOperation(concat, "Hello", " ", "World")); // Output: Hello World

Tipado de funciones currificadas

Las funciones currificadas requieren un tipado especial que refleje la cadena de funciones:

// Tipo para una función currificada de 3 parámetros
type Curried<A, B, C, R> = (a: A) => (b: B) => (c: C) => R;

// Función helper para currificar
function curry<A, B, C, R>(
  fn: (a: A, b: B, c: C) => R
): Curried<A, B, C, R> {
  return (a: A) => (b: B) => (c: C) => fn(a, b, c);
}

// Función original
function formatText(prefix: string, text: string, suffix: string): string {
  return `${prefix}${text}${suffix}`;
}

// Versión currificada
const curriedFormat = curry(formatText);

// Uso
const addHeader = curriedFormat("# ");
const addHeaderAndFooter = addHeader("Important Message");
console.log(addHeaderAndFooter(" !!!")); // Output: # Important Message !!!

Tipado de funciones de composición

La composición de funciones requiere un tipado cuidadoso para mantener la seguridad de tipos a lo largo de la cadena:

// Función de composición tipada
function compose<A, B, C>(
  f: (b: B) => C,
  g: (a: A) => B
): (a: A) => C {
  return (a: A) => f(g(a));
}

// Versión que acepta múltiples funciones
function pipe<T>(...fns: Array<(arg: T) => T>): (arg: T) => T {
  return (arg: T) => fns.reduce((result, fn) => fn(result), arg);
}

// Funciones para componer
const addOne = (n: number): number => n + 1;
const multiply = (n: number): number => n * 2;
const toString = (n: number): string => `Result: ${n}`;

// Composición con tipos correctos
const addThenMultiply = compose(multiply, addOne);
const processAndFormat = compose(toString, addThenMultiply);

console.log(processAndFormat(5)); // Output: Result: 12

Uso de tipos condicionales para funciones avanzadas

Los tipos condicionales nos permiten crear tipados más sofisticados para funciones de orden superior:

// Tipo que extrae el tipo de retorno de una función
type ReturnTypeOf<T extends (...args: any[]) => any> = T extends (...args: any[]) => infer R ? R : never;

// Función que transforma el valor de retorno de otra función
function transformResult<F extends (...args: any[]) => any, T>(
  fn: F,
  transformer: (result: ReturnTypeOf<F>) => T
): (...args: Parameters<F>) => T {
  return (...args: Parameters<F>): T => {
    const result = fn(...args);
    return transformer(result);
  };
}

// Función original
function calculateArea(width: number, height: number): number {
  return width * height;
}

// Transformamos su resultado
const formatArea = transformResult(
  calculateArea,
  (area) => `The area is ${area} square units`
);

console.log(formatArea(5, 4)); // Output: The area is 20 square units

Aquí usamos infer para extraer el tipo de retorno y Parameters<F> para obtener los tipos de los parámetros.

Tipado de funciones asíncronas de orden superior

Cuando trabajamos con funciones asíncronas, debemos asegurarnos de tipar correctamente las promesas:

// Función que recibe una función asíncrona
async function withRetry<T>(
  operation: () => Promise<T>,
  maxRetries: number = 3
): Promise<T> {
  let lastError: Error | undefined;
  
  for (let attempt = 1; attempt <= maxRetries; attempt++) {
    try {
      return await operation();
    } catch (error) {
      console.log(`Attempt ${attempt} failed. ${maxRetries - attempt} retries left.`);
      lastError = error as Error;
    }
  }
  
  throw new Error(`Operation failed after ${maxRetries} attempts: ${lastError?.message}`);
}

// Uso
async function fetchUserData(userId: string): Promise<{ id: string, name: string }> {
  // Simulación de fetch que puede fallar
  if (Math.random() < 0.7) {
    throw new Error("Network error");
  }
  return { id: userId, name: "John Doe" };
}

// Con tipado correcto
withRetry(() => fetchUserData("user123"))
  .then(user => console.log(`User loaded: ${user.name}`))
  .catch(error => console.error(`Failed to load user: ${error.message}`));

Uso de tipos de utilidad para mejorar el tipado

TypeScript proporciona tipos de utilidad que facilitan el tipado de funciones de orden superior:

// Usando tipos de utilidad de TypeScript
function partial<T extends (...args: any[]) => any>(
  fn: T,
  ...args: Partial<Parameters<T>>
): (...args: Partial<Parameters<T>>) => ReturnType<T> {
  return (...restArgs: any[]) => {
    const allArgs = [...args];
    
    // Rellenamos los argumentos faltantes
    for (let i = args.length; i < fn.length; i++) {
      allArgs[i] = restArgs[i - args.length];
    }
    
    return fn(...allArgs);
  };
}

// Función original
function greet(greeting: string, name: string, punctuation: string): string {
  return `${greeting}, ${name}${punctuation}`;
}

// Aplicación parcial
const sayHello = partial(greet, "Hello");
const sayHelloToJohn = partial(greet, "Hello", "John");

console.log(sayHello("World", "!")); // Output: Hello, World!
console.log(sayHelloToJohn("!")); // Output: Hello, John!

Aquí usamos Parameters<T> y ReturnType<T> para extraer los tipos de parámetros y retorno.

Mejores prácticas para el tipado de funciones de orden superior

Para finalizar, algunas mejores prácticas a seguir:

  • Usa genéricos para crear funciones flexibles pero con tipado seguro
  • Define tipos explícitos para funciones complejas mediante type o interface
  • Aprovecha la inferencia de tipos cuando sea posible para reducir la verbosidad
  • Utiliza tipos de utilidad como Parameters<T> y ReturnType<T> para manipular tipos
  • Documenta los parámetros genéricos para mejorar la comprensión del código
  • Evita el uso de any y prefiere tipos genéricos o unknown cuando sea necesario
  • Considera el uso de tipos condicionales para casos avanzados
// Ejemplo de buenas prácticas
// Definimos tipos claros
type Mapper<T, U> = (item: T) => U;
type Predicate<T> = (item: T) => boolean;

// Documentamos los genéricos
/**
 * Filtra y transforma elementos de un array
 * @template T Tipo de los elementos de entrada
 * @template U Tipo de los elementos de salida
 */
function filterAndMap<T, U>(
  array: T[],
  predicate: Predicate<T>,
  mapper: Mapper<T, U>
): U[] {
  return array
    .filter(predicate)
    .map(mapper);
}

// Uso con inferencia de tipos
const numbers = [1, 2, 3, 4, 5];
const evenNumbersDoubled = filterAndMap(
  numbers,
  n => n % 2 === 0,
  n => n * 2
);

console.log(evenNumbersDoubled); // Output: [4, 8]

Con estas técnicas y patrones, podrás aprovechar al máximo el sistema de tipos de TypeScript para crear funciones de orden superior robustas, flexibles y seguras.

CONSTRUYE TU CARRERA EN IA Y PROGRAMACIÓN SOFTWARE

Accede a +1000 lecciones y cursos con certificado. Mejora tu portfolio con certificados de superación para tu CV.

30 % DE DESCUENTO

Plan mensual

19.00 /mes

13.30 € /mes

Precio normal mensual: 19 €
63 % DE DESCUENTO

Plan anual

10.00 /mes

7.00 € /mes

Ahorras 144 € al año
Precio normal anual: 120 €
Aprende TypeScript online

Ejercicios de esta lección Funciones de primera clase y orden superior

Evalúa tus conocimientos de esta lección Funciones de primera clase y orden superior con nuestros retos de programación de tipo Test, Puzzle, Código y Proyecto con VSCode, guiados por IA.

Funciones

TypeScript
Test

Reto composición de funciones

TypeScript
Código

Reto tipos especiales

TypeScript
Código

Reto tipos genéricos

TypeScript
Código

Módulos

TypeScript
Test

Polimorfismo

TypeScript
Código

Funciones TypeScript

TypeScript
Código

Interfaces

TypeScript
Puzzle

Funciones puras

TypeScript
Puzzle

Reto namespaces

TypeScript
Código

Funciones flecha

TypeScript
Puzzle

Polimorfismo

TypeScript
Test

Operadores

TypeScript
Test

Conversor de unidades

TypeScript
Proyecto

Funciones flecha

TypeScript
Test

Control de flujo

TypeScript
Código

Herencia

TypeScript
Puzzle

Clases

TypeScript
Puzzle

Proyecto validación de tipado

TypeScript
Proyecto

Clases y objetos

TypeScript
Código

Encapsulación

TypeScript
Test

Herencia

TypeScript
Test

Proyecto sistema de votación

TypeScript
Proyecto

Reto genéricos con clases

TypeScript
Código

Inmutabilidad

TypeScript
Puzzle

Interfaces

TypeScript
Test

Funciones de alto orden

TypeScript
Test

Reto map y filter

TypeScript
Código

Control de flujo

TypeScript
Test

Interfaces

TypeScript
Código

Reto funciones orden superior

TypeScript
Código

Herencia y clases abstractas

TypeScript
Código

Reto tipos mapped

TypeScript
Código

Herencia de clases

TypeScript
Código

Reto funciones puras

TypeScript
Código

Variables y constantes

TypeScript
Puzzle

Introducción a TypeScript

TypeScript
Test

Reto testing unitario

TypeScript
Código

Funciones de primera clase

TypeScript
Puzzle

Clases

TypeScript
Test

OOP y CRUD en TypeScript

TypeScript
Proyecto

Interfaces y su implementación

TypeScript
Código

Tipos genéricos

TypeScript
Test

Namespaces

TypeScript
Test

Proyecto calculadora gastos

TypeScript
Proyecto

Operadores y expresiones

TypeScript
Código

Proyecto generador de contraseñas

TypeScript
Proyecto

Reto unión e intersección

TypeScript
Código

Encapsulación

TypeScript
Puzzle

Tipos de unión e intersección

TypeScript
Test

Tipos de unión e intersección

TypeScript
Puzzle

Reto hola mundo en TS

TypeScript
Código

Variables y constantes

TypeScript
Código

Funciones puras

TypeScript
Test

Control de flujo

TypeScript
Código

Introducción a TypeScript

TypeScript
Código

Resolución de módulos

TypeScript
Test

Control de flujo

TypeScript
Puzzle

Reto tipos de utilidad

TypeScript
Código

Reto tipos literales y condicionales

TypeScript
Código

Reto exportar e importar

TypeScript
Código

Propiedades y métodos

TypeScript
Código

Tipos de utilidad

TypeScript
Test

Clases y objetos

TypeScript
Código

Tipos de datos, variables y constantes

TypeScript
Código

Proyecto Minigestor de tareas

TypeScript
Proyecto

Operadores

TypeScript
Puzzle

Funciones flecha y contexto

TypeScript
Código

Funciones

TypeScript
Puzzle

Reto type aliases

TypeScript
Código

Funciones de alto orden

TypeScript
Puzzle

Funciones y parámetros tipados

TypeScript
Código

Tipos literales

TypeScript
Puzzle

Reto enums

TypeScript
Código

Tipos de utilidad

TypeScript
Puzzle

Modificadores de acceso y encapsulación

TypeScript
Código

Polimorfismo

TypeScript
Puzzle

Tipos genéricos

TypeScript
Puzzle

Reto módulos

TypeScript
Código

Tipos literales

TypeScript
Test

Inmutabilidad

TypeScript
Test

Proyecto Generator de datos

TypeScript
Proyecto

Variables y constantes

TypeScript
Test

Funciones de primera clase

TypeScript
Test

Todas las lecciones de TypeScript

Accede a todas las lecciones de TypeScript y aprende con ejemplos prácticos de código y ejercicios de programación con IDE web sin instalar nada.

Introducción A Typescript

TypeScript

Introducción Y Entorno

Instalación Y Configuración De Typescript

TypeScript

Introducción Y Entorno

Tipos De Datos, Variables Y Constantes

TypeScript

Sintaxis

Operadores Y Expresiones

TypeScript

Sintaxis

Control De Flujo

TypeScript

Sintaxis

Funciones Y Parámetros Tipados

TypeScript

Sintaxis

Funciones Flecha Y Contexto

TypeScript

Sintaxis

Enums

TypeScript

Sintaxis

Type Aliases Y Aserciones De Tipo

TypeScript

Sintaxis

Clases Y Objetos

TypeScript

Programación Orientada A Objetos

Interfaces Y Su Implementación

TypeScript

Programación Orientada A Objetos

Modificadores De Acceso Y Encapsulación

TypeScript

Programación Orientada A Objetos

Herencia Y Clases Abstractas

TypeScript

Programación Orientada A Objetos

Polimorfismo

TypeScript

Programación Orientada A Objetos

Decoradores Básicos

TypeScript

Programación Orientada A Objetos

Propiedades Y Métodos

TypeScript

Programación Orientada A Objetos

Inmutabilidad

TypeScript

Programación Funcional

Funciones Puras

TypeScript

Programación Funcional

Funciones De Primera Clase

TypeScript

Programación Funcional

Funciones De Alto Orden

TypeScript

Programación Funcional

Conceptos Básicos E Inmutabilidad

TypeScript

Programación Funcional

Funciones De Primera Clase Y Orden Superior

TypeScript

Programación Funcional

Composición De Funciones

TypeScript

Programación Funcional

Métodos Funcionales De Arrays (Map, Filter, Reduce)

TypeScript

Programación Funcional

Tipos Literales

TypeScript

Tipos Intermedios Y Avanzados

Tipos Genéricos

TypeScript

Tipos Intermedios Y Avanzados

Tipos De Unión E Intersección

TypeScript

Tipos Intermedios Y Avanzados

Tipos De Utilidad

TypeScript

Tipos Intermedios Y Avanzados

Unknown, Never Y Tipos Especiales

TypeScript

Tipos Intermedios Y Avanzados

Tipos Mapped

TypeScript

Tipos Intermedios Y Avanzados

Genéricos Con Clases E Interfaces

TypeScript

Tipos Intermedios Y Avanzados

Módulos

TypeScript

Namespaces Y Módulos

Namespaces

TypeScript

Namespaces Y Módulos

Resolución De Módulos

TypeScript

Namespaces Y Módulos

Exportación E Importación De Módulos

TypeScript

Namespaces Y Módulos

Introducción A Módulos

TypeScript

Namespaces Y Módulos

Testing Unitario En Typescript

TypeScript

Testing

Accede GRATIS a TypeScript y certifícate

En esta lección

Objetivos de aprendizaje de esta lección

  • Comprender el concepto de funciones como valores y su importancia en la programación funcional en TypeScript
  • Dominar técnicas para trabajar con funciones como parámetros y crear funciones de orden superior
  • Aprender a implementar funciones que retornan funciones, aprovechando conceptos como closures y currying
  • Aplicar un tipado correcto y seguro a las funciones de orden superior usando genéricos y tipos avanzados
  • Implementar patrones funcionales comunes como composición, memoización y creación de middlewares