Async/await в TypeScript

от автора

Если вас заинтересовала эта статья, то вы, наверное, несколько разбираетесь в асинхронном программировании на JavaScript и, возможно, интересуетесь, как оно работает в TypeScript.

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

В сущности, async/await – это синтаксический сахар для промисов, то есть, ключевое слово async/await обертывает промисы. Функция async всегда возвращает промис. Даже если пропустить ключевое слово Promise, компилятор обернет вашу функцию в немедленно разрешаемый промис.

Давайте покажу:

const myAsynFunction = async (url: string): Promise<T> => {     const { data } = await fetch(url)     return data } 
const immediatelyResolvedPromise = (url: string) => {     const resultPromise = new Promise((resolve, reject) => {         resolve(fetch(url))     })     return  resultPromise } 

Пусть они и выглядят совершенно по-разному, два вышеприведенных фрагмента кода более-менее эквивалентны. Async/await просто позволяет писать код в более синхронной манере и избавляет от необходимости встраивать промис в строку. Это очень мощный прием, если имеешь дело со сложными асинхронными паттернами.

Чтобы выжать максимум из синтаксиса async/await, нужно иметь базовое представление о промисах. Давайте подробнее рассмотрим, что представляют собой промисы на фундаментальном уровне.

Что такое промис в TypeScript?

В переводе с английского «promise» означает «обещание». В JavaScript промис описывает ожидание того, что некоторое событие произойдет в определенный момент, и ваше приложение полагается на результат этого будущего события при выполнении определенных других задач.

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

Допустим, мне нужно покосить газон. Звоню в газонокосильную компанию, где мне обещают, что через пару часов придет человек и покосит газон. Я, в свою очередь, обещаю сразу же ему за это заплатить, при условии, что мой газон будет выкошен как следует.

Заметили паттерн? Первая очевидная вещь, которую нужно отметить – второе событие полностью полагается на первое. Если будет выполнено обещание, заложенное в первом событии, то выполнится и следующее событие. Промис в том событии либо выполняется, либо не выполняется, либо остается в подвешенном состоянии.

Рассмотрим эту последовательность шаг за шагом и выразим ее в коде.

Синтаксис промиса

Прежде, чем написать весь код, давайте разберемся в синтаксисе промиса – конкретно, такого промиса, который разрешается в строку.

Мы объявили promise при помощи ключевого слова new + Promise, где промис принимает аргументы resolve и reject. Теперь давайте напишем промис, выражающий события из вышеприведенной блок-схемы.

// Я отправляю запрос в компанию. Он синхронный // Компания обещает мне выполнить работу const angelMowersPromise = new Promise<string>((resolve, reject) => {     // Обещание разрешилось спустя несколько часов     setTimeout(() => {         resolve('We finished mowing the lawn')     }, 100000) // разрешается спустя 100 000 мс     reject("We couldn't mow the lawn") })  const myPaymentPromise = new Promise<Record<string, number | string>>((resolve, reject) => {     // разрешившийся промис с объектом: платежом в 1000 евро     // и большое спасибо     setTimeout(() => {         resolve({             amount: 1000,             note: 'Thank You',         })     }, 100000)     // промис отклонен. 0 евро и отзыв «неудовлетворительно»      reject({         amount: 0,         note: 'Sorry Lawn was not properly Mowed',     }) }) 

В вышеприведенном коде объявлены как обещания компании, так и наши обещания. Обещание компании либо выполняется через 100 000 мс, либо отклоняется. Promise всегда находится в одном из трех состояний: resolved, если ошибки нет, rejected, если встретилась ошибка, или pending, если обещание promise пока ни отклонено, ни выполнено. В нашем случае все это укладывается в период 100000ms.

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

Последовательное выполнение с .then

Теперь можно сцепить промисы, что позволяет выполнять их последовательно с применением .then. Эти функции похожи на обычный человеческий язык: сделай так, а затем вот это, а потом то и так далее.

angelMowersPromise     .then(() => myPaymentPromise.then(res => console.log(res)))     .catch(error => console.log(error)) 

Вышеприведенный код выполнит angelMowersPromise. Если с этим ошибки не случится, он выполнит myPaymentPromise. Если в одном из двух промисов возникнет ошибка, то она будет отловлена в блоке catch.

Теперь давайте рассмотрим более технический пример. При программировании клиентского интерфейса есть типичная задача: выполнять запросы по сети и адекватно реагировать на их результаты.

Ниже – запрос, требующий выбрать список сотрудников с удаленного сервера.

const api =  'http://dummy.restapiexample.com/api/v1/employees'    fetch(api)     .then(response => response.json())     .then(employees => employees.forEach(employee => console.log(employee.id)) // логирует id всех сотрудников     .catch(error => console.log(error.message))) // логирует любую ошибку, приходящую от промиса 

Бывает так, что необходимо параллельно или последовательно выполнять сразу множество обещаний. В подобных сценариях особенно полезны такие конструкции как Promise.all или Promise.race.

Представьте, к примеру, сто нужно выбрать список из 1 000 пользователей GitHub, а затем сделать дополнительный запрос с ID, чтобы выбрать для каждого из них аватарки. Совсем не обязательно вы захотите дожидаться завершения этих операций со всеми пользователями в последовательности; вам нужны только все выбранные аватарки. Мы подробнее поговорим об этом ниже, когда будем обсуждать Promise.all.

Теперь, когда вы в общем и целом поняли, что такое промисы, давайте рассмотрим синтаксис async/await.

async/await

Синтаксис Async/await удивительно прост при работе с промисами. Он предоставляет простой интерфейс для чтения и записи промисов, причем, таким образом, что они кажутся синхронными.

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

Как понятно из названия, async с await всегда ходят парой. То есть, делать await можно только внутри функции async. Функция async сообщает компилятору, что это асинхронная функция.

Если преобразовать вышеприведенные промисы, то получится такой синтаксис:

const myAsync = async (): Promise<Record<string, number | string>> => {     await angelMowersPromise     const response = await myPaymentPromise     return response }

Сразу заметно, что этот код выглядит более удобочитаемым и кажется синхронным. В строке 3 мы сказали компилятору дожидаться выполнения angelMowersPromise, и только потом делать что-то еще. Затем возвращаем отклик от myPaymentPromise.

Возможно, вы заметили, что здесь мы пропустили обработку ошибок. Это можно было бы сделать в блоке catch после .then в промисе. Но что делать, если нам попадется ошибка? Это приводит нас к блоку try/catch.

Обработка ошибок в try/catch

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

Допустим, например, что у нас лег сервер, либо что мы отправили запрос в неверном формате. Мы должны приостановить выполнение, чтобы предотвратить обвал программы. Синтаксис будет выглядеть так:

interface Employee {     id: number     employee_name: string     employee_salary: number     employee_age: number     profile_image: string } const fetchEmployees = async (): Promise<Array<Employee> | string> => {     const api = 'http://dummy.restapiexample.com/api/v1/employees'     try {         const response = await fetch(api)         const { data } = await response.json()         return data     } catch (error) {         if (error) {             return error.message         }     } } 

Мы инициировали функцию async. В качестве возвращаемого значения ожидаем массив типа typeof с информацией о сотрудниках, либо строку с сообщениями об ошибке. Соответственно, тип Promise формулируется как Promise<Array<Employee> | string>.

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

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

Конкурентное выполнение при помощи Promise.all

Как я говорил ранее, бывает, что обещания должны выполняться параллельно.

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

В данном случае мы воспользуемся Promise.all. Как пишет Mozilla, “Promise.all обычно применяется после того, как было запущено множество асинхронных задач, которые должны работать конкурентно, и после того, как пообещали, каковы будут их результаты – чтобы можно было дождаться, пока все эти задачи будут завершены.”

В псевдокоде было бы что-то подобное:

  • Выбрать всех пользователей => /employee

  • Дождаться всех данных о пользователях. Извлечь id от каждого пользователя. Выбрать каждого пользователя => /employee/{id}

  • Сгенерировать электронное сообщение для каждого пользователя по его имени

const baseApi = 'https://reqres.in/api/users?page=1' const userApi = 'https://reqres.in/api/user'  const fetchAllEmployees = async (url: string): Promise<Employee[]> => {     const response = await fetch(url)     const { data } = await response.json()     return data }  const fetchEmployee = async (url: string, id: number): Promise<Record<string, string>> => {     const response = await fetch(`${url}/${id}`)     const { data } = await response.json()     return data } const generateEmail = (name: string): string => {     return `${name.split(' ').join('.')}@company.com` }  const runAsyncFunctions = async () => {     try {         const employees = await fetchAllEmployees(baseApi)         Promise.all(             employees.map(async user => {                 const userName = await fetchEmployee(userApi, user.id)                 const emails = generateEmail(userName.name)                 return emails             })         )     } catch (error) {         console.log(error)     } } runAsyncFunctions()

В вышеприведенном коде fetchEmployees выбирает всех сотрудников из  baseApi. Мы ожидаем отклик (await), преобразуем его в JSON, а затем возвращаем преобразованные данные.

Самое важное, о чем здесь нужно помнить – как мы последовательно выполняли код строка за строкой внутри функции async с ключевым словом await. Мы бы получили ошибку, если бы попытались преобразовать в JSON данные, которых дождались не полностью. То же касается fetchEmployee, с той оговоркой, что выбирали бы всего одного сотрудника. Более интересен фрагмент runAsyncFunctions, где все асинхронные функции выполняются конкурентно.

Сначала обернем в блок try/catch все методы, находящиеся внутри runAsyncFunctions. Далее ждем (await) результат выбора всех сотрудников. Нам нужен id каждого сотрудника, чтобы выбрать соответствующие им данные, но в конечном счете нам нужна именно информация о сотрудниках.

Вот где можно прибегнуть к Promise.all, чтобы конкурентно обработать все Promises. Каждый fetchEmployee Promise конкурентно выполняется для всех сотрудников. Информация о сотрудниках, которую мы дождемся, используется для генерации электронного сообщения от каждого сотрудника, это делается при помощи функции generateEmail.

Если случится ошибка, то она распространяется как обычно, от невыполненного обещания к Promise.all, а затем превращается в исключение, которое можно отловить в блоке catch.

Ключевые выводы

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

Завершу статью несколькими ключевыми тезисами; помните о них, когда будете работать над вашим следующим асинхронным проектом на TypeScript.

  • await работает только внутри функции async 

  • Функция, помеченная ключевым словом async, всегда возвращает Promise

  • Если возвращаемое значение внутри async не возвращает Promise, то оно будет обернуто в немедленно разрешаемый Promise

  • Как только встретится ключевое слово await, выполнение приостанавливается, пока не будет завершено Promise 

  • await либо вернет результат от выполненного Promise, либо выбросит исключение от отклоненного Promise


ссылка на оригинал статьи https://habr.com/ru/articles/585236/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *