Async/await в TypeScript

Моя цель - предложение широкого ассортимента товаров и услуг на постоянно высоком качестве обслуживания по самым выгодным ценам.

Прежде чем перейти к статье, хочу вам представить, экономическую онлайн игру Brave Knights, в которой вы можете играть и зарабатывать. Регистируйтесь, играйте и зарабатывайте!

Если вас заинтересовала эта статья, то вы, наверное, несколько разбираетесь в асинхронном программировании на 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/company/piter/blog/585236/


Интересные статьи

Интересные статьи

В прошлой статье меня упрекнули, что я при живом Vue 3 пишу про "устаревший" Vue 2. Отговорившись тем, что Vue 3 еще не production-ready, я понемногу начал его смотреть и...
Часто от программистов PHP можно услышать: «О нет! Только не „Битрикс“!». Многие специалисты не хотят связываться фреймворком, считают его некрасивым и неудобным. Однако вакансий ...
Цель — показать, где TS дает иллюзию безопасности позволяя получить ошибки во время работы программы. Мы не будем говорить о багах, в TS их достаточно 1,500 open bugs and 6,000 closed (‘is:is...
В преддверии старта курса «Разработчик C#» подготовили перевод интересного материала. Async/Await — Введение Языковая конструкция Async/Await существует со времен C# версии 5.0 (2012) и б...
Довольно часто владельцы сайтов просят поставить на свои проекты индикаторы курсов валют и их динамику. Можно воспользоваться готовыми информерами, но они не всегда позволяют должным образом настроить...