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

  1. ¿Por qué es importante la programación asíncrona?

  2. Cómo TypeScript facilita la programación asíncrona

  3. Cómo usar Promesas en TypeScript

  4. Cómo usar Async / Await en TypeScript

  5. Cómo usar Callbacks en TypeScript

  6. Conclusión

¿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:

  1. 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)

  2. 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
    }
  1. 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;
  1. Considere las bibliotecas de flujo de control
    Para operaciones asíncronas complejas, use utilidades como async.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: