Если вас заинтересовала эта статья, то вы, наверное, несколько разбираетесь в асинхронном программировании на 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 при помощи ключевого слова 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. Без него функции просто выполняются в том же порядке, в котором и разрешаются.
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 всегда возвращает 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.
Допустим, например, что у нас лег сервер, либо что мы отправили запрос в неверном формате. Мы должны приостановить выполнение, чтобы предотвратить обвал программы. Синтаксис будет выглядеть так:
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. Если какое-то исключение ускользнет, то может получиться код, плохо поддающийся отладке, либо даже может быть испорчена вся программа.
Продолжим пример с нашим API для выбора сотрудников. Допустим, нам нужно выбрать всех сотрудников, затем выбрать их имена, затем сгенерировать на основе имен электронные сообщения. Очевидно, нам нужно выполнять эти функции в синхронной манере, но при этом параллельно, чтобы одна функция не блокировала другую.
В данном случае мы воспользуемся Promise.all. Как пишет Mozilla, “Promise.all обычно применяется после того, как было запущено множество асинхронных задач, которые должны работать конкурентно, и после того, как пообещали, каковы будут их результаты – чтобы можно было дождаться, пока все эти задачи будут завершены.”
В псевдокоде было бы что-то подобное:
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.
Завершу статью несколькими ключевыми тезисами; помните о них, когда будете работать над вашим следующим асинхронным проектом на 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 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
Async/await в TypeScript
Если вас заинтересовала эта статья, то вы, наверное, несколько разбираетесь в асинхронном программировании на JavaScript и, возможно, интересуетесь, как оно работает в TypeScript. Поскольку TypeScript...
habr.com