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ícateFunciones 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
ointerface
- Aprovecha la inferencia de tipos cuando sea posible para reducir la verbosidad
- Utiliza tipos de utilidad como
Parameters<T>
yReturnType<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 ounknown
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.
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
Reto composición de funciones
Reto tipos especiales
Reto tipos genéricos
Módulos
Polimorfismo
Funciones TypeScript
Interfaces
Funciones puras
Reto namespaces
Funciones flecha
Polimorfismo
Operadores
Conversor de unidades
Funciones flecha
Control de flujo
Herencia
Clases
Proyecto validación de tipado
Clases y objetos
Encapsulación
Herencia
Proyecto sistema de votación
Reto genéricos con clases
Inmutabilidad
Interfaces
Funciones de alto orden
Reto map y filter
Control de flujo
Interfaces
Reto funciones orden superior
Herencia y clases abstractas
Reto tipos mapped
Herencia de clases
Reto funciones puras
Variables y constantes
Introducción a TypeScript
Reto testing unitario
Funciones de primera clase
Clases
OOP y CRUD en TypeScript
Interfaces y su implementación
Tipos genéricos
Namespaces
Proyecto calculadora gastos
Operadores y expresiones
Proyecto generador de contraseñas
Reto unión e intersección
Encapsulación
Tipos de unión e intersección
Tipos de unión e intersección
Reto hola mundo en TS
Variables y constantes
Funciones puras
Control de flujo
Introducción a TypeScript
Resolución de módulos
Control de flujo
Reto tipos de utilidad
Reto tipos literales y condicionales
Reto exportar e importar
Propiedades y métodos
Tipos de utilidad
Clases y objetos
Tipos de datos, variables y constantes
Proyecto Minigestor de tareas
Operadores
Funciones flecha y contexto
Funciones
Reto type aliases
Funciones de alto orden
Funciones y parámetros tipados
Tipos literales
Reto enums
Tipos de utilidad
Modificadores de acceso y encapsulación
Polimorfismo
Tipos genéricos
Reto módulos
Tipos literales
Inmutabilidad
Proyecto Generator de datos
Variables y constantes
Funciones de primera clase
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
Introducción Y Entorno
Instalación Y Configuración De Typescript
Introducción Y Entorno
Tipos De Datos, Variables Y Constantes
Sintaxis
Operadores Y Expresiones
Sintaxis
Control De Flujo
Sintaxis
Funciones Y Parámetros Tipados
Sintaxis
Funciones Flecha Y Contexto
Sintaxis
Enums
Sintaxis
Type Aliases Y Aserciones De Tipo
Sintaxis
Clases Y Objetos
Programación Orientada A Objetos
Interfaces Y Su Implementación
Programación Orientada A Objetos
Modificadores De Acceso Y Encapsulación
Programación Orientada A Objetos
Herencia Y Clases Abstractas
Programación Orientada A Objetos
Polimorfismo
Programación Orientada A Objetos
Decoradores Básicos
Programación Orientada A Objetos
Propiedades Y Métodos
Programación Orientada A Objetos
Inmutabilidad
Programación Funcional
Funciones Puras
Programación Funcional
Funciones De Primera Clase
Programación Funcional
Funciones De Alto Orden
Programación Funcional
Conceptos Básicos E Inmutabilidad
Programación Funcional
Funciones De Primera Clase Y Orden Superior
Programación Funcional
Composición De Funciones
Programación Funcional
Métodos Funcionales De Arrays (Map, Filter, Reduce)
Programación Funcional
Tipos Literales
Tipos Intermedios Y Avanzados
Tipos Genéricos
Tipos Intermedios Y Avanzados
Tipos De Unión E Intersección
Tipos Intermedios Y Avanzados
Tipos De Utilidad
Tipos Intermedios Y Avanzados
Unknown, Never Y Tipos Especiales
Tipos Intermedios Y Avanzados
Tipos Mapped
Tipos Intermedios Y Avanzados
Genéricos Con Clases E Interfaces
Tipos Intermedios Y Avanzados
Módulos
Namespaces Y Módulos
Namespaces
Namespaces Y Módulos
Resolución De Módulos
Namespaces Y Módulos
Exportación E Importación De Módulos
Namespaces Y Módulos
Introducción A Módulos
Namespaces Y Módulos
Testing Unitario En Typescript
Testing
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