Изучите асинхронное программирование на TypeScript: Promises, Async/Await и обратные вызовы

Асинхронное программирование - это парадигма программирования, которая позволяет писать код, выполняющийся асинхронно. В отличие от синхронного программирования, при котором код выполняется последовательно, асинхронное программирование позволяет выполнять код в фоновом режиме, в то время как остальная часть программы продолжает выполняться. Это особенно полезно для выполнения задач, выполнение которых может занять много времени, таких как извлечение данных из удаленного API.

Асинхронное программирование необходимо для создания адаптивных и эффективных приложений на JavaScript. TypeScript, надмножество JavaScript, еще больше упрощает работу с асинхронным программированием.

Существует несколько подходов к асинхронному программированию на TypeScript, включая использование promises, async/await и обратных вызовов. Мы подробно рассмотрим каждый из этих подходов, чтобы вы могли выбрать оптимальный для вашего случая использования.

содержание

  1. Почему важно асинхронное программирование?
  2. Как TypeScript упрощает асинхронное программирование
  3. Как использовать Promises в TypeScript Как создать Promise Как связать Promises в цепочку
  4. Как использовать Async / Await в TypeScript
  5. Как использовать обратные вызовы в TypeScript
  6. Вывод

Почему важно асинхронное программирование?

Асинхронное программирование имеет решающее значение для создания адаптивных и эффективных веб-приложений. Оно позволяет задачам выполняться в фоновом режиме, в то время как остальная часть программы продолжает работать, позволяя пользовательскому интерфейсу реагировать на вводимые данные. Кроме того, асинхронное программирование может повысить общую производительность, позволяя нескольким задачам выполняться одновременно.

Существует множество реальных примеров асинхронного программирования, таких как доступ к камерам и микрофонам пользователей и обработка событий пользовательского ввода. Даже если вы не часто создаете асинхронные функции, важно знать, как правильно их использовать, чтобы обеспечить надежность и высокую производительность вашего приложения.

Как TypeScript упрощает асинхронное программирование

TypeScript предлагает несколько функций, упрощающих асинхронное программирование, включая безопасность типов, вывод типов, проверку типов и аннотации типов.

Благодаря безопасности типов вы можете гарантировать, что ваш код будет работать должным образом, даже при работе с асинхронными функциями. Например, TypeScript может перехватывать ошибки, связанные с нулевыми и неопределенными значениями, во время компиляции, экономя ваше время и усилия при отладке.

Вывод и проверка типов в TypeScript также сокращают объем шаблонного кода, который вам нужно написать, делая ваш код более кратким и удобным для чтения.

А аннотации к типам в TypeScript обеспечивают ясность и документацию для вашего кода, что особенно полезно при работе с асинхронными функциями, которые могут быть сложными для понимания.

Теперь давайте углубимся в изучение этих трех ключевых особенностей асинхронного программирования: promises, async/await и обратные вызовы.

Как использовать Promises в TypeScript

Promises - это мощный инструмент для обработки асинхронных операций в TypeScript. Например, вы можете использовать promise для получения данных из внешнего API или для выполнения трудоемкой задачи в фоновом режиме, пока ваш основной поток продолжает работать.

Чтобы использовать Promise, вы создаете новый экземпляр класса Promise и передаете ему функцию, которая выполняет асинхронную операцию. Эта функция должна вызывать метод resolve с результатом в случае успешного выполнения операции или метод reject с ошибкой в случае сбоя.

Как только Promise будет создан, вы можете привязать к нему обратные вызовы, используя метод then. Эти обратные вызовы будут запущены, когда Promise будет выполнено, с переданным в качестве параметра значением resolved. Если обещание отклонено, вы можете прикрепить обработчик ошибок, используя метод catch, который будет вызван с указанием причины отклонения.

Использование Promises дает ряд преимуществ по сравнению с традиционными методами, основанными на обратном вызове. Например, Promises может помочь предотвратить "ад обратного вызова", распространенную проблему в асинхронном коде, когда вложенные обратные вызовы становятся сложными для чтения и поддержки.

Обещания также упрощают обработку ошибок в асинхронном коде, поскольку вы можете использовать метод catch для управления ошибками, возникающими в любом месте цепочки обещаний.

Наконец, Promises могут упростить ваш код, предоставляя согласованный, компонуемый способ обработки асинхронных операций, независимо от их базовой реализации.

Как создать обещание

Синтаксис обещания:

const myPromise = new Promise((resolve, reject) => {
  // Do some asynchronous operation
  // If the operation is successful, call resolve with the result
  // If the operation fails, call reject with an error object
});

myPromise
  .then((result) => {
    // Handle the successful result
  })
  .catch((error) => {
    // Handle the error
  });
// Example 1 on how to create a promise

function myAsyncFunction(): Promise<string> {
  return new Promise<string>((resolve, reject) => {
    // Some asynchronous operation
    setTimeout(() => {
      // Successful operation resolves promiseCheck out my latest blog post on mastering async programming in TypeScript! Learn how to work with Promises, Async/Await, and Callbacks to write efficient and scalable code. Get ready to take your TypeScript skills to the next level!
      const success = true;

      if (success) {
        // Resolve the promise with the operation result if the operation was successful
        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}`;
        // Reject the promise with the operation result if the operation failed
        reject(new Error(rejectMessage));
      }
    }, 2000);
  });
}

// Use the promise
myAsyncFunction()
  .then((result) => {
    console.log(result); // output : The result is success and your operation result is 4
  })
  .catch((error) => {
    console.error(error); // output : The result is failed and your operation result is 404
  });

В приведенном выше примере у нас есть функция myAsyncFunction(), которая возвращает promise. Для создания promise мы используем конструктор Promise, который принимает функцию обратного вызова с аргументами resolve и reject. Если асинхронная операция выполнена успешно, мы вызываем функцию resolve. Если это не удается, мы вызываем функцию отклонения.

Объект promise, возвращаемый конструктором, имеет метод then(), который принимает функции обратного вызова success и failure. Если promise выполняется успешно, вызывается функция обратного вызова success с результатом. Если promise отклоняется, вызывается функция обратного вызова failure с сообщением об ошибке.

Объект promise также имеет метод catch(), используемый для обработки ошибок, возникающих во время цепочки обещаний. Метод catch() использует функцию обратного вызова, которая вызывается при возникновении какой-либо ошибки в цепочке обещаний.

Теперь давайте перейдем к тому, как создавать цепочки обещаний в TypeScript.

Как связать обещания в цепочку

Объединение promises в цепочку позволяет выполнять несколько асинхронных операций последовательно или параллельно. Это полезно, когда вам нужно выполнить несколько асинхронных задач одну за другой или одновременно. Например, вам может потребоваться асинхронно получать данные, а затем обрабатывать их асинхронно.

Давайте рассмотрим пример того, как связать обещания в цепочку:

// Example on how chaining promises works
// First promise
const promise1 = new Promise((resolve, reject) => {
  const functionOne: string = "This is the first promise function";
  setTimeout(() => {
    resolve(functionOne);
  }, 1000);
});

// Second promise
const promise2 = (data: number) => {
  const functionTwo: string = "This is the second second promise  function";
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      resolve(` ${data}  '+'  ${functionTwo} `);
    }, 1000);
  });
};

// Chaining first and second promises together
promise1
  .then(promise2)
  .then((result) => {
    console.log(result); // output: This is the first promise function + This is the second second promise function
  })
  .catch((error) => {
    console.error(error);
  });

В приведенном выше примере у нас есть два promises: promise1 и promise2. promise1 завершается через 1 секунду со строкой "Это первая функция promise". promise2 принимает число в качестве входных данных и возвращает promise, который через 1 секунду преобразуется в строку, объединяющую введенное число и строку "Это вторая функция promise".

Мы связываем два promises вместе, используя метод then. Выходные данные promise1 передаются в качестве входных данных promise2. Наконец, мы снова используем метод then, чтобы записать выходные данные promise2 в консоль. Если promise1 или promise2 отклоняются, ошибка будет перехвачена методом catch.

Поздравляю! Вы узнали, как создавать и связывать promises в TypeScript. Теперь вы можете использовать promises для выполнения асинхронных операций в TypeScript. Теперь давайте рассмотрим, как работает функция Async/Await в TypeScript.

Как использовать Async / Await в TypeScript

Async/await - это синтаксис, введенный в ES2017 для упрощения работы с Promises. Он позволяет писать асинхронный код, который выглядит и ощущается как синхронный код.

В TypeScript вы можете определить асинхронную функцию, используя ключевое слово async. Это сообщает компилятору, что функция является асинхронной и вернет обещание.

Теперь давайте посмотрим, как использовать async/await в TypeScript.

Синтаксис асинхронности/ ожидания:

// Async / Await Syntax in TypeScript
async function functionName(): Promise<ReturnType> {
  try {
    const result = await promise;
    // code to execute after promise resolves
    return result;
  } catch (error) {
    // code to execute if promise rejects
    throw error;
  }
}

В приведенном выше примере functionName - это асинхронная функция, которая возвращает обещание ReturnType. Ключевое слово await используется для ожидания разрешения обещания перед переходом к следующей строке кода.

Блок try/catch используется для обработки любых ошибок, возникающих при выполнении кода внутри асинхронной функции. Если произойдет ошибка, она будет перехвачена блоком catch, где вы сможете обработать ее соответствующим образом.

Использование функций со стрелками с асинхронностью / ожиданием

Вы также можете использовать функции со стрелками с синтаксисом async/await в TypeScript:

const functionName = async (): Promise<ReturnType> => {
  try {
    const result = await promise;
    // code to execute after promise resolves
    return result;
  } catch (error) {
    // code to execute if promise rejects
    throw error;
  }
};

В приведенном выше примере functionName определяется как функция со стрелкой, которая возвращает обещание ReturnType. Ключевое слово async указывает, что это асинхронная функция, а ключевое слово await используется для ожидания разрешения обещания перед переходом к следующей строке кода.

Асинхронное выполнение / ожидание с вызовом API

Теперь давайте выйдем за рамки синтаксиса и извлекем некоторые данные из API с помощью 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();

Здесь мы извлекаем данные из API JSONPlaceholder, преобразуем их в JSON и затем записываем в консоль. Это реальный пример того, как использовать async/await в TypeScript.

Вы должны увидеть информацию о пользователе в консоли. На этом рисунке показан вывод:

Асинхронность/ожидание с вызовом Axios API

// Example 2 on how to use async / await in 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();

В приведенном выше примере мы определяем функцию fetchApi(), используя async/await и метод Axios.get(), чтобы отправить HTTP-запрос GET по указанному URL-адресу. Мы используем await для ожидания ответа, затем извлекаем данные, используя свойство data объекта response. Наконец, мы записываем данные в консоль с помощью console.log(). Любые возникающие ошибки перехватываются и регистрируются в консоли с помощью console.error().

Мы можем добиться этого с помощью Axios, так что вы должны увидеть тот же результат в консоли.

На этом рисунке показаны выходные данные при использовании Axios в консоли:

Примечание: Прежде чем вы попробуете использовать приведенный выше код, вам необходимо установить Axios с помощью npm или yarn.


npm install axios

yarn add axios

Если вы не знакомы с Axios, вы можете узнать о нем больше здесь.

Вы можете видеть, что мы использовали блок try и catch для обработки ошибок. Блок try и catch - это метод управления ошибками в TypeScript. Поэтому всякий раз, когда вы выполняете вызовы API, как это только что сделали мы, убедитесь, что вы используете блок try и catch для обработки любых ошибок.

Теперь давайте рассмотрим более продвинутое использование блоков try и catch в TypeScript:

// Example 3 on how to use async / await in 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; // Return the recipes array
  } catch (error) {
    console.error("Error fetching recipes:", error);
    if (error instanceof Error) {
      return error.message;
    }
    return "An unknown error occurred.";
  }
};

// Fetch and log recipes
fetchRecipes().then((data) => {
  if (Array.isArray(data)) {
    console.log("Recipes fetched successfully:", data);
  } else {
    console.error("Error message:", data);
  }
});

В приведенном выше примере мы определяем рецепт интерфейса, который описывает структуру данных, ожидаемых от API. Затем мы создаем функцию fetchRecipes(), используя async/await и метод fetch(), чтобы отправить HTTP-запрос GET к указанной конечной точке API.

Мы используем блок try/catch для обработки любых ошибок, которые могут возникнуть во время запроса API. Если запрос выполняется успешно, мы извлекаем свойство data из ответа с помощью await и возвращаем его. При возникновении ошибки мы проверяем наличие сообщения об ошибке и возвращаем его в виде строки, если оно существует.

Наконец, мы вызываем функцию fetchRecipes() и используем .then() для записи возвращенных данных в консоль. Этот пример демонстрирует, как использовать async/await с блоками try/catch для обработки ошибок в более продвинутом сценарии, где нам нужно извлечь данные из объекта response и вернуть пользовательское сообщение об ошибке.

На этом изображении показан результат выполнения кода:

Асинхронное выполнение / ожидание с помощью Promise.all

Promise.all() - это метод, который принимает массив promises в качестве входных данных (iterable) и возвращает одно Promise в качестве выходных данных. Это обещание выполняется, когда все входные promises были разрешены или если входная iterable не содержит обещаний. Он немедленно отклоняет запрос, если какое-либо из введенных обещаний отклонено или если не-обещания выдают ошибку, и он будет отклонен с первым сообщением об отклонении или ошибке.

// Example of using async / await with 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));

В приведенном выше коде мы использовали Promise.all для одновременного получения нескольких API. Если вам нужно получить несколько API, вы можете использовать Promise.all, чтобы получить их все сразу. Как вы можете видеть, мы использовали map для перебора массива API, а затем передали его в Promise.all, чтобы получить их одновременно.

На рисунке ниже показаны выходные данные вызовов API:

Давайте посмотрим, как использовать Promise.все это с помощью Axios:

// Example of using async / await with axios and 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();

В приведенном выше примере мы используем Promise.all для одновременной выборки данных с двух разных URL-адресов. Сначала мы создаем массив URL-адресов, затем используем map для создания массива Promises из вызовов axios.get. Мы передаем этот массив в Promise.all, который возвращает массив ответов. Наконец, мы снова используем map, чтобы получить данные из каждого ответа и записать их в консоль.

Как использовать обратные вызовы в TypeScript

Обратный вызов - это функция, передаваемая в качестве аргумента другой функции. Функция обратного вызова выполняется внутри другой функции. Обратные вызовы гарантируют, что функция не будет запущена до завершения задачи, а будет запущена сразу после завершения задачи. Они помогают нам писать асинхронный JavaScript-код и предотвращать проблемы и ошибки.

// Example of using callbacks in typescript

const add = (a: number, b: number, callback: (result: number) => void) => {
  const result = a + b;
  callback(result);
};

add(10, 20, (result) => {
  console.log(result);
});

На рисунке ниже показана функция обратного вызова:

Давайте рассмотрим еще один пример использования обратных вызовов в TypeScript:

// Example of using a callback function in 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);
    });
};

// Usage of fetchUserData with a callback function
fetchUserData(1, (error, user) => {
  if (error) {
    console.error(error);
  } else {
    console.log(user);
  }
});

В приведенном выше примере у нас есть функция fetchUserData, которая принимает идентификатор и обратный вызов в качестве параметров. Этот обратный вызов является функцией с двумя параметрами: ошибкой и пользователем.

Функция fetchUserData извлекает пользовательские данные из конечной точки API JSONPlaceholder, используя идентификатор. Если выборка выполняется успешно, она создает объект User и передает его функции обратного вызова с ошибкой null. Если во время выборки возникает ошибка, она отправляет сообщение об ошибке в функцию обратного вызова с нулевым пользователем.

Чтобы использовать функцию fetchUserData с обратным вызовом, мы предоставляем идентификатор и функцию обратного вызова в качестве аргументов. Функция обратного вызова проверяет наличие ошибок и регистрирует пользовательские данные, если ошибок нет.

На рисунке ниже показаны выходные данные вызовов API:

Как ответственно использовать обратные вызовы

Хотя обратные вызовы являются фундаментальными для асинхронного программирования на TypeScript, они требуют тщательного управления, чтобы избежать "ада обратного вызова" - кода в форме пирамиды с глубокими вложениями, который становится трудным для чтения и поддержки. Вот как эффективно использовать обратные вызовы:

  1. Избегайте глубокой вложенности Упростите структуру кода, разбив сложные операции на именованные функции Используйте promises или async/await для сложных асинхронных рабочих процессов (подробнее об этом ниже).
  2. Сначала обработайте ошибки Всегда соблюдайте Node.js соглашение о параметрах (error, result) Проверяйте наличие ошибок на каждом уровне вложенных обратных вызовов
    function processData(input: string, callback: (err: Error | null, result?: string) => void) {
      // ... always call callback with error first
    }
  1. Используйте аннотации к типам Используйте систему типов TypeScript для принудительного использования сигнатур обратного вызова Определите понятные интерфейсы для параметров обратного вызова
    type ApiCallback = (error: Error | null, data?: ApiResponse) => void;
  1. Рассмотрите библиотеки потоков управления Для сложных асинхронных операций, используйте утилиты, подобные async.js для: Параллельного выполнения Последовательного выполнения Конвейеров обработки ошибок

Когда использовать обратные вызовы и Альтернативы

Бывают моменты, когда обратные вызовы являются отличным выбором, а бывают и такие, когда это не так.

Обратные вызовы полезны, когда вы работаете с асинхронными операциями (однократное завершение), взаимодействуете со старыми библиотеками или API, которые ожидают обратных вызовов, обрабатываете прослушиватели событий (например, прослушиватели кликов или события websocket) или создаете легкие утилиты с простыми асинхронными потребностями.

В других сценариях, где вам нужно сосредоточиться на написании поддерживаемого кода с четким асинхронным потоком, обратные вызовы вызывают проблемы, и вам следует предпочесть promises или async-await. Например, когда вам нужно связать несколько операций, обработать распространение сложных ошибок, работать с современными API (такими как Fetch API или FS Promises) или использовать promise.all() для параллельного выполнения.

Пример перехода от обратных вызовов к обещаниям:

// Callback version
function fetchUser(id: number, callback: (err: Error | null, user?: User) => void) {
  // ... 
}

// Promise version
async function fetchUserAsync(id: number): Promise<User> {
  // ...
}

// Usage with async/await
try {
  const user = await fetchUserAsync(1);
} catch (error) {
  // Handle error
}

Эволюция асинхронных паттернов

PatternProsCons
CallbacksSimple, universalNested complexity
PromisesChainable, better error flowRequires .then() chains
Async/AwaitSync-like readabilityRequires transpilation

В современных проектах на TypeScript часто используется сочетание: обратные вызовы для шаблонов, управляемых событиями, и promises/async-await для сложной асинхронной логики. Ключевым моментом является выбор правильного инструмента для вашего конкретного случая использования при сохранении ясности кода.

Вывод

В этой статье мы узнали о различных способах обработки асинхронного кода в TypeScript. Мы узнали об обратных вызовах, promises, async/await и о том, как их использовать в TypeScript. Мы также познакомились с этой концепцией.

Если вы хотите узнать больше о программировании и о том, как стать лучшим инженером-программистом, вы можете подписаться на мой YouTube-канал CliffTech.

Спасибо, что прочитали мою статью. Надеюсь, она вам понравилась. Если у вас возникнут какие-либо вопросы, не стесняйтесь обращаться ко мне.

Свяжитесь со мной в социальных сетях:

  • Твиттер
  • Гитхаб
  • Сеть LinkedIn