La programación asíncrona es un paradigma de programación que te permite escribir código que se ejecuta asíncronamente
. A diferencia de la programación síncrona, que ejecuta el código de manera secuencial, la programación asíncrona permite que el código se ejecute en segundo plano mientras el resto del programa continúa en ejecución. Esto es especialmente útil para tareas que pueden tardar mucho tiempo en completarse, como la obtención de datos de una API remota.
La programación asíncrona
es esencial para crear aplicaciones receptivas y eficientes en JavaScript. TypeScript, un superconjunto de JavaScript, facilita aún más el trabajo con la programación asíncrona.
Existen varios enfoques para la programación asíncrona
en TypeScript, incluidos el uso de promesas
, async/await
y callbacks
. Cubriremos cada uno de estos enfoques en detalle para que puedas elegir el/los mejor(es) para tu caso de uso.
Tabla de Contenidos
¿Por qué es importante la programación asíncrona?
La programación asíncrona es crucial para construir aplicaciones web receptivas y eficientes. Permite que las tareas se ejecuten en segundo plano mientras el resto del programa continúa, manteniendo la interfaz de usuario receptiva a la entrada. Además, la programación asíncrona puede mejorar el rendimiento general al permitir que múltiples tareas se ejecuten al mismo tiempo.
Existen muchos ejemplos del mundo real de programación asíncrona, como el acceso a cámaras y micrófonos de usuarios y la gestión de eventos de entrada de usuario. Incluso si no creas funciones asíncronas con frecuencia, es importante saber cómo utilizarlas correctamente para asegurarte de que tu aplicación sea confiable y funcione bien.
Cómo TypeScript facilita la programación asíncrona
TypeScript ofrece varias características que simplifican la programación asíncrona, incluyendo seguridad de tipos
, inferencia de tipos
, verificación de tipos
y anotaciones de tipos
.
Con la seguridad de tipos, puedes asegurarte de que tu código se comporte como se espera, incluso al tratar con funciones asíncronas. Por ejemplo, TypeScript puede detectar errores relacionados con valores nulos y no definidos en tiempo de compilación, ahorrándote tiempo y esfuerzo en la depuración.
La inferencia y verificación de tipos de TypeScript también reducen la cantidad de código repetitivo que necesitas escribir, haciendo que tu código sea más conciso y fácil de leer.
Y las anotaciones de tipos de TypeScript proporcionan claridad y documentación para tu código, lo cual es especialmente útil al trabajar con funciones asíncronas que pueden ser complejas de entender.
Ahora vamos a sumergirnos y aprender acerca de estas tres características clave de la programación asíncrona: promesas, async/await y devoluciones de llamada.
Cómo usar Promesas en TypeScript
Las promesas son una herramienta poderosa para manejar operaciones asíncronas en TypeScript. Por ejemplo, podrías usar una promesa para obtener datos de una API externa o para realizar una tarea que consume tiempo en segundo plano mientras tu hilo principal sigue funcionando.
Para usar una Promesa, creas una nueva instancia de la clase Promise
y le pasas una función que realiza la operación asíncrona. Esta función debe llamar al método resolve con el resultado cuando la operación tenga éxito, o al método reject con un error si falla.
Una vez creada la Promesa, puedes adjuntar devoluciones de llamada a ella usando el método then
. Estas devoluciones de llamada se activarán cuando la Promesa se cumpla, con el valor resuelto pasado como parámetro. Si la Promesa es rechazada, puedes adjuntar un controlador de errores usando el método catch, que se llamará con la razón del rechazo.
Usar Promesas ofrece varias ventajas sobre los métodos tradicionales basados en devoluciones de llamada. Por ejemplo, las Promesas pueden ayudar a prevenir el “infierno de devoluciones de llamada”, un problema común en el código asíncrono donde las devoluciones de llamada anidadas se vuelven difíciles de leer y mantener.
Las Promesas también facilitan el manejo de errores en el código asíncrono, ya que puedes usar el método catch para gestionar errores que ocurran en cualquier parte de la cadena de Promesas.
Finalmente, las Promesas pueden simplificar tu código al proporcionar una forma consistente y componible de manejar operaciones asíncronas, independientemente de su implementación subyacente.
Cómo crear una promesa
Sintaxis de la promesa:
const myPromise = new Promise((resolve, reject) => {
// Realizar alguna operación asíncrona
// Si la operación tiene éxito, llamar a resolve con el resultado
// Si la operación falla, llamar a reject con un objeto de error
});
myPromise
.then((result) => {
// Manejar el resultado exitoso
})
.catch((error) => {
// Manejar el error
});
// Ejemplo 1 sobre cómo crear una promesa
function myAsyncFunction(): Promise<string> {
return new Promise<string>((resolve, reject) => {
// Alguna operación asíncrona
setTimeout(() => {
// La operación exitosa resuelve la promesa¡Echa un vistazo a mi última publicación en el blog sobre el dominio de la programación asíncrona en TypeScript! Aprende a trabajar con Promesas, Async/Await y Callbacks para escribir un código eficiente y escalable. ¡Prepárate para llevar tus habilidades de TypeScript al siguiente nivel!
const success = true;
if (success) {
// Resolver la promesa con el resultado de la operación si la operación fue exitosa
resolve(
`The result is success and your operation result is ${operationResult}`
);
} else {
const rejectCode: number = 404;
const rejectMessage: string = `The result is failed and your operation result is ${rejectCode}`;
// Rechazar la promesa con el resultado de la operación si la operación falló
reject(new Error(rejectMessage));
}
}, 2000);
});
}
// Utilizar la promesa
myAsyncFunction()
.then((result) => {
console.log(result); // resultado: El resultado es éxito y el resultado de tu operación es 4
})
.catch((error) => {
console.error(error); // resultado: El resultado es fallido y el resultado de tu operación es 404
});
En el ejemplo anterior, tenemos una función llamada myAsyncFunction()
que devuelve una promesa
. Utilizamos el constructor Promise
para crear la promesa, el cual recibe una función de devolución de llamada
con los argumentos resolve
y reject
. Si la operación asincrónica es exitosa, llamamos a la función resolve. Si falla, llamamos a la función reject.
El objeto de promesa devuelto por el constructor tiene un método then()
, que recibe funciones de devolución de llamada para éxito y fallo. Si la promesa se resuelve exitosamente, se llama a la función de devolución de llamada de éxito con el resultado. Si la promesa es rechazada, se llama a la función de devolución de llamada de fallo con un mensaje de error.
El objeto de promesa también tiene un método catch()
que se utiliza para manejar errores que ocurran durante la cadena de promesas. El método catch()
recibe una función de devolución de llamada, la cual se llama si ocurre algún error en la cadena de promesas.
Ahora, pasemos a ver cómo encadenar promesas en TypeScript.
Cómo encadenar promesas
Encadenar promesas te permite realizar múltiples operaciones asincrónicas
en secuencia o en paralelo. Esto es útil cuando necesitas llevar a cabo varias tareas asíncronas una después de otra o al mismo tiempo. Por ejemplo, podrías necesitar buscar datos de forma asíncrona y luego procesarlos de forma asíncrona.
Veamos un ejemplo de cómo encadenar promesas:
// Ejemplo de cómo funciona el encadenamiento de promesas
// Primera promesa
const promise1 = new Promise((resolve, reject) => {
const functionOne: string = "This is the first promise function";
setTimeout(() => {
resolve(functionOne);
}, 1000);
});
// Segunda promesa
const promise2 = (data: number) => {
const functionTwo: string = "This is the second second promise function";
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve(` ${data} '+' ${functionTwo} `);
}, 1000);
});
};
// Encadenando la primera y la segunda promesa
promise1
.then(promise2)
.then((result) => {
console.log(result); // salida: Esta es la función de la primera promesa + Esta es la función de la segunda promesa
})
.catch((error) => {
console.error(error);
});
En el ejemplo anterior, tenemos dos promesas: promise1
y promise2
. promise1
se resuelve después de 1 segundo con la cadena “Esta es la función de la primera promesa.” promise2
toma un número como entrada y devuelve una promesa que se resuelve después de 1 segundo con una cadena que combina el número de entrada y la cadena “Esta es la función de la segunda promesa.”
Encadenamos las dos promesas utilizando el método then
. La salida de promise1
se pasa como entrada a promise2
. Finalmente, usamos el método then
nuevamente para registrar la salida de promise2
en la consola. Si promise1
o promise2
se rechazan, el error será capturado por el método catch
.
¡Felicidades! Has aprendido cómo crear y encadenar promesas en TypeScript. Ahora puedes usar promesas para realizar operaciones asincrónicas en TypeScript. Ahora, exploremos cómo funciona Async/Await
en TypeScript.
Cómo usar Async / Await en TypeScript
Async/await es una sintaxis introducida en ES2017 para facilitar el trabajo con Promesas. Permite escribir código asíncrono que se ve y se siente como código sincrónico.
En TypeScript, puedes definir una función asíncrona utilizando la palabra clave async
. Esto le indica al compilador que la función es asíncrona y devolverá una Promesa.
Ahora, veamos cómo usar async/await en TypeScript.
Sintaxis de Async / Await:
// Sintaxis de Async / Await en TypeScript
async function functionName(): Promise<ReturnType> {
try {
const result = await promise;
// código a ejecutar después de que la promesa se resuelva
return result;
} catch (error) {
// código a ejecutar si la promesa es rechazada
throw error;
}
}
En el ejemplo anterior, functionName
es una función asíncrona que devuelve una Promesa de ReturnType
. La palabra clave await
se utiliza para esperar a que la promesa se resuelva antes de pasar a la siguiente línea de código.
El bloque try/catch
se utiliza para manejar cualquier error que ocurra mientras se ejecuta el código dentro de la función asíncrona. Si ocurre un error, será capturado por el bloque catch, donde puedes manejarlo de manera apropiada.
Usando Funciones Flecha con Async / Await
También puedes utilizar funciones flecha con la sintaxis de async/await en TypeScript:
const functionName = async (): Promise<ReturnType> => {
try {
const result = await promise;
// código a ejecutar después de que la promesa se resuelva
return result;
} catch (error) {
// código a ejecutar si la promesa es rechazada
throw error;
}
};
En el ejemplo anterior, functionName
se define como una función de flecha que devuelve una Promesa de ReturnType
. La palabra clave async indica que esta es una función asíncrona, y la palabra clave await se utiliza para esperar a que la promesa se resuelva antes de pasar a la siguiente línea de código.
Async / Await con una llamada a la API
Ahora, vamos más allá de la sintaxis y obtenemos algunos datos de una API utilizando async/await.
interface User {
id: number;
name: string;
email: string;
}
const fetchApi = async (): Promise<void> => {
try {
const response = await fetch("https://jsonplaceholder.typicode.com/users");
if (!response.ok) {
throw new Error(
`Failed to fetch users (HTTP status code: ${response.status})`
);
}
const data: User[] = await response.json();
console.log(data);
} catch (error) {
console.error(error);
throw error;
}
};
fetchApi();
Aquí, estamos obteniendo datos de la API JSONPlaceholder, convirtiéndolos a JSON y luego registrándolos en la consola. Este es un ejemplo del mundo real de cómo usar async/await en TypeScript.
Deberías ver información del usuario en la consola. Esta imagen muestra la salida:
Async/Await con llamada a la API de Axios
// Ejemplo 2 de cómo usar async / await en TypeScript
const fetchApi = async (): Promise<void> => {
try {
const response = await axios.get(
"https://jsonplaceholder.typicode.com/users"
);
const data = await response.data;
console.log(data);
} catch (error) {
console.error(error);
}
};
fetchApi();
En el ejemplo anterior, definimos la función fetchApi()
utilizando async/await y el método Axios.get()
para realizar una solicitud HTTP GET a la URL especificada. Usamos await para esperar la respuesta, luego extraemos los datos utilizando la propiedad data del objeto de respuesta. Finalmente, registramos los datos en la consola con console.log()
. Cualquier error que ocurra se captura y se registra en la consola con console.error()
.
Podemos lograr esto utilizando Axios, así que deberías ver el mismo resultado en la consola.
Esta imagen muestra la salida al usar Axios en la consola:
Nota: Antes de intentar el código anterior, necesitas instalar Axios usando npm o yarn.
npm install axios
yarn add axios
Si no estás familiarizado con Axios, puedes aprender más sobre ello aquí.
Puedes ver que usamos un bloque try
y catch
para manejar errores. El bloque try
y catch
es un método para gestionar errores en TypeScript. Así que, cada vez que realices llamadas a la API como lo hicimos, asegúrate de usar un bloque try
y catch
para manejar cualquier error.
Ahora, exploremos un uso más avanzado del bloque try
y catch
en TypeScript:
// Ejemplo 3 sobre cómo usar async / await en TypeScript
interface Recipe {
id: number;
name: string;
ingredients: string[];
instructions: string[];
prepTimeMinutes: number;
cookTimeMinutes: number;
servings: number;
difficulty: string;
cuisine: string;
caloriesPerServing: number;
tags: string[];
userId: number;
image: string;
rating: number;
reviewCount: number;
mealType: string[];
}
const fetchRecipes = async (): Promise<Recipe[] | string> => {
const api = "https://dummyjson.com/recipes";
try {
const response = await fetch(api);
if (!response.ok) {
throw new Error(`Failed to fetch recipes: ${response.statusText}`);
}
const { recipes } = await response.json();
return recipes; // Retornar el array de recetas
} catch (error) {
console.error("Error fetching recipes:", error);
if (error instanceof Error) {
return error.message;
}
return "An unknown error occurred.";
}
};
// Obtener y registrar recetas
fetchRecipes().then((data) => {
if (Array.isArray(data)) {
console.log("Recipes fetched successfully:", data);
} else {
console.error("Error message:", data);
}
});
En el ejemplo anterior, definimos una interface Recipe
que describe la estructura de los datos que esperamos de la API. Luego creamos la función fetchRecipes()
utilizando async/await y el método fetch() para hacer una solicitud HTTP GET al punto final de la API especificado.
Utilizamos un bloque try/catch
para manejar cualquier error que pueda ocurrir durante la solicitud a la API. Si la solicitud es exitosa, extraemos la propiedad de datos de la respuesta usando await y la retornamos. Si ocurre un error, verificamos si hay un mensaje de error y lo retornamos como una cadena si existe.
Finalmente, llamamos a la función fetchRecipes()
y utilizamos .then()
para imprimir los datos devueltos en la consola. Este ejemplo demuestra cómo usar async/await
con bloques try/catch
para manejar errores en un escenario más avanzado, donde necesitamos extraer datos de un objeto de respuesta y devolver un mensaje de error personalizado.
Esta imagen muestra el resultado de la ejecución del código:
Async / Await con Promise.all
Promise.all()
es un método que toma un array de promesas como entrada (un iterable) y devuelve una única Promesa como salida. Esta Promesa se resuelve cuando todas las promesas de entrada han sido resueltas o si el iterable de entrada no contiene promesas. Se rechaza inmediatamente si alguna de las promesas de entrada es rechazada o si las no promesas arrojan un error, y se rechazará con el primer mensaje de rechazo o error.
// Ejemplo de uso de async / await con Promise.all
interface User {
id: number;
name: string;
email: string;
profilePicture: string;
}
interface Post {
id: number;
title: string;
body: string;
}
interface Comment {
id: number;
postId: number;
name: string;
email: string;
body: string;
}
const fetchApi = async <T>(url: string): Promise<T> => {
try {
const response = await fetch(url);
if (response.ok) {
const data = await response.json();
return data;
} else {
throw new Error(`Network response was not ok for ${url}`);
}
} catch (error) {
console.error(error);
throw new Error(`Error fetching data from ${url}`);
}
};
const fetchAllApis = async (): Promise<[User[], Post[], Comment[]]> => {
try {
const [users, posts, comments] = await Promise.all([
fetchApi<User[]>("https://jsonplaceholder.typicode.com/users"),
fetchApi<Post[]>("https://jsonplaceholder.typicode.com/posts"),
fetchApi<Comment[]>("https://jsonplaceholder.typicode.com/comments"),
]);
return [users, posts, comments];
} catch (error) {
console.error(error);
throw new Error("Error fetching data from one or more APIs");
}
};
fetchAllApis()
.then(([users, posts, comments]) => {
console.log("Users: ", users);
console.log("Posts: ", posts);
console.log("Comments: ", comments);
})
.catch((error) => console.error(error));
En el código anterior, utilizamos Promise.all
para obtener múltiples APIs al mismo tiempo. Si tienes varias APIs para obtener, puedes usar Promise.all
para obtenerlas todas a la vez. Como puedes ver, utilizamos map
para recorrer el array de APIs y luego pasarlo a Promise.all
para obtenerlas simultáneamente.
La imagen a continuación muestra la salida de las llamadas a la API:
Vamos a ver cómo usar Promise.all
con Axios:
// Ejemplo de uso de async / await con axios y Promise.all
const fetchApi = async () => {
try {
const urls = [
"https://jsonplaceholder.typicode.com/users",
"https://jsonplaceholder.typicode.com/posts",
];
const responses = await Promise.all(urls.map((url) => axios.get(url)));
const data = await Promise.all(responses.map((response) => response.data));
console.log(data);
} catch (error) {
console.error(error);
}
};
fetchApi();
En el ejemplo anterior, estamos utilizando Promise.all
para obtener datos de dos URL diferentes al mismo tiempo. Primero, creamos un arreglo de URL, luego usamos map para crear un arreglo de Promesas a partir de las llamadas a axios.get
. Pasamos este arreglo a Promise.all
, que devuelve un arreglo de respuestas. Finalmente, usamos map nuevamente para obtener los datos de cada respuesta y registrarlos en la consola.
Cómo usar callbacks en TypeScript
Un callback es una función pasada como argumento a otra función. La función callback se ejecuta dentro de la otra función. Los callbacks aseguran que una función no se ejecute antes de que una tarea esté completada, pero que se ejecute justo después de que la tarea finalice. Nos ayudan a escribir código JavaScript asíncrono y a prevenir problemas y errores.
// Ejemplo de uso de callbacks en TypeScript
const add = (a: number, b: number, callback: (result: number) => void) => {
const result = a + b;
callback(result);
};
add(10, 20, (result) => {
console.log(result);
});
La imagen a continuación muestra la función callback:
Veamos otro ejemplo de uso de callbacks en TypeScript:
// Ejemplo de uso de una función callback en TypeScript
type User = {
name: string;
email: string;
};
const fetchUserData = (
id: number,
callback: (error: Error | null, user: User | null) => void
) => {
const api = `https://jsonplaceholder.typicode.com/users/${id}`;
fetch(api)
.then((response) => {
if (response.ok) {
return response.json();
} else {
throw new Error("Network response was not ok.");
}
})
.then((data) => {
const user: User = {
name: data.name,
email: data.email,
};
callback(null, user);
})
.catch((error) => {
callback(error, null);
});
};
// Uso de fetchUserData con una función callback
fetchUserData(1, (error, user) => {
if (error) {
console.error(error);
} else {
console.log(user);
}
});
En el ejemplo anterior, tenemos una función llamada fetchUserData
que toma un id
y un callback
como parámetros. Este callback
es una función con dos parámetros: un error y un usuario.
La función fetchUserData
recupera datos de usuario de un punto final de API de JSONPlaceholder utilizando el id
. Si la recuperación es exitosa, crea un objeto User
y lo pasa a la función de callback con un error nulo. Si hay un error durante la recuperación, envía el error a la función de callback con un usuario nulo.
Para usar la función fetchUserData
con una callback, proporcionamos un id
y una función de callback como argumentos. La función de callback verifica si hay errores y registra los datos del usuario si no hay errores.
La imagen a continuación muestra la salida de las llamadas a la API:
Cómo usar callbacks de manera responsable
Si bien los callbacks son fundamentales para la programación asíncrona en TypeScript, requieren una gestión cuidadosa para evitar “el infierno de los callbacks” – el código en forma de pirámide, profundamente anidado, que se vuelve difícil de leer y mantener. Aquí te mostramos cómo usar los callbacks de manera efectiva:
-
Evita la anidación profunda
-
Aplana la estructura de tu código dividiendo operaciones complejas en funciones nombradas
-
Utiliza promesas o async/await para flujos de trabajo asíncronos complejos (más sobre esto a continuación)
-
-
Manejo de errores primero
-
Siempre siga la convención de Node.js de parámetros
(error, resultado)
-
Revise errores en cada nivel de devolución de llamada anidada
-
function processData(input: string, callback: (err: Error | null, result?: string) => void) {
// ... siempre llame primero a la devolución de llamada con el error
}
-
Usar anotaciones de tipo
-
Aproveche el sistema de tipos de TypeScript para hacer cumplir las firmas de devolución de llamada
-
Defina interfaces claras para los parámetros de devolución de llamada
-
type ApiCallback = (error: Error | null, data?: ApiResponse) => void;
-
Considere las bibliotecas de flujo de control
Para operaciones asíncronas complejas, use utilidades comoasync.js
para:-
Ejecución paralela
-
Ejecución en serie
-
Manejo de tuberías de errores
-
Cuándo usar callbacks frente a alternativas
Hay momentos en los que los callbacks son una excelente elección y otros momentos en los que no lo son.
Los callbacks son útiles cuando se trabaja con operaciones asíncronas (conclusión única), se interactúa con bibliotecas antiguas o APIs que esperan callbacks, se manejan escuchas de eventos (como escuchas de clics o eventos de websocket) o se crean utilidades ligeras con necesidades asíncronas simples.
En otros escenarios donde se necesita centrarse en escribir código mantenible con un flujo asíncrono claro, los callbacks causan problemas y se deben preferir promesas o async-await. Por ejemplo, cuando se necesita encadenar múltiples operaciones, manejar una propagación de errores compleja, trabajar con APIs modernas (como la API Fetch o FS Promises) o usar promise.all()
para ejecución en paralelo.
Ejemplo de migración de callbacks a promesas:
// Versión de callback
function fetchUser(id: number, callback: (err: Error | null, user?: User) => void) {
// ...
}
// Versión de promesas
async function fetchUserAsync(id: number): Promise<User> {
// ...
}
// Uso con async/await
try {
const user = await fetchUserAsync(1);
} catch (error) {
// Manejar errores
}
La Evolución de los Patrones Asíncronos
Patrón | Ventajas | Desventajas |
Callbacks | Sencillo, universal | Complejidad anidada |
Promesas | Encadenables, mejor flujo de errores | Requiere cadenas .then() |
Async/Await | Lectura similar a la sincronía | Requiere transpileación |
Los proyectos modernos de TypeScript a menudo utilizan una mezcla: callbacks para patrones impulsados por eventos y promesas/async-await para lógica asíncrona compleja. La clave es elegir la herramienta adecuada para tu caso de uso específico mientras se mantiene la claridad del código.
Conclusión
En este artículo, hemos aprendido sobre las diferentes formas de manejar código asíncrono en TypeScript. Hemos aprendido sobre callbacks, promesas, async/await, y cómo usarlos en TypeScript. También hemos aprendido sobre este concepto.
Si deseas aprender más sobre programación y cómo convertirte en un mejor ingeniero de software, puedes suscribirte a mi canal de YouTube CliffTech.
Gracias por leer mi artículo. Espero que lo hayas disfrutado. Si tienes alguna pregunta, no dudes en contactarme.
Conéctate conmigo en las redes sociales: