Если вас заинтересовала эта статья, то вы, наверное, несколько разбираетесь в асинхронном программировании на 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
конкурентно выполняется для всех сотрудников. Информация о сотрудниках, которую мы дождемся, используется для генерации электронного сообщения от каждого сотрудника, это делается при помощи функции generateEmai
l.
Если случится ошибка, то она распространяется как обычно, от невыполненного обещания к Promise.all
, а затем превращается в исключение, которое можно отловить в блоке catch
.
Ключевые выводы
async
и await
позволяет писать асинхронный код так, что он выглядит и действует как синхронный. Такой код становится гораздо проще читать, писать и судить о нем.
Завершу статью несколькими ключевыми тезисами; помните о них, когда будете работать над вашим следующим асинхронным проектом на TypeScript
.
await
работает только внутри функцииasync
Функция, помеченная ключевым словом
async
, всегда возвращаетPromise
Если возвращаемое значение внутри
async
не возвращаетPromise
, то оно будет обернуто в немедленно разрешаемыйPromise
Как только встретится ключевое слово
await
, выполнение приостанавливается, пока не будет завершеноPromise
await
либо вернет результат от выполненногоPromise
, либо выбросит исключение от отклоненногоPromise
Комментарии (18)
Metotron0
25.10.2021 14:44+1Вопрос по языку: зачем после Promise написали обобщённый тип <T>, если он больше нигде не упоминается? Я думал, это нужно, чиобы обрзначить, что на выходе или у какого-то параметра такой же тип, как на входе, но здесь ни один параметр не имеет тип T и на выходе его нет.
Xazzzi
25.10.2021 15:11Ошибка же, там дожен был быть
unknown
илиany
на худой конец.Metotron0
26.10.2021 01:58А, это какой-то обёрнутый в скрытую логику тип данных TS, которому нужно передать, что будет после разрешения промиса?
Xazzzi
26.10.2021 11:05+1Вы правильно пишете, тип
Т
неоткуда взять. ВместоPromise<T>
для результата можно прописатьPromise<unknown>
, тогда значение которое выдаст await будет иметь типunknown
и не получится его случайно использовать не проверив на соответствие типу (например через type predicate), в отличии отany
.
faiwer
25.10.2021 17:24+10const 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) } }
вы тут
await
передPromise.all
забыли. Без него всё насмарку.upd1. и ещё забыли
return
, иначе какой смысл писатьreturn emails
.
upd2.и скорее всего нуженпеременная названа криво, т.к. email один, должно быть.flat()
а то у вас странный списокemail[][]
получается :-)email
(безs
).
Delagen
26.10.2021 11:05Первый же пример "более менее" не эквивалентен. Первый возвращает свойство data от результата fetch, а второй просто результат.
qrKot
26.10.2021 21:45-1Ну ребят, ну чесслово, издательский дом... Вы этот текст сюда принесли для того, чтобы на корректорах сэкономить?
В переводе с английского «promise» означает «обещание». В JavaScript промис описывает ожидание того, что некоторое событие произойдет в определенный момент, и ваше приложение полагается на результат этого будущего события при выполнении определенных других задач.
...
Заметили паттерн? Первая очевидная вещь, которую нужно отметить – второе событие полностью полагается на первое. Если будет выполнено обещание, заложенное в первом событии, то выполнится и следующее событие. Промис в том событии либо выполняется, либо не выполняется, либо остается в подвешенном состоянии.
...
Мы объявили
promise
при помощи ключевого словаnew + Promise
, где промис принимает аргументыresolve
иreject
. Теперь давайте напишем промис, выражающий события из вышеприведенной блок-схемы....
Теперь можно сцепить промисы, что позволяет выполнять их последовательно с применением
.then
. Эти функции похожи на обычный человеческий язык: сделай так, а затем вот это, а потом то и так далее.Шел 16-й (шестнадцатый) абзац пространной статьи об асинхронном программировании в TS, но до сих пор ни разу не было упомянуто, чем все эти городки с Promise'ами отличаются от if/then/else.
Мы рассказали про порядок выполнения промисов, про то, как их chain'ить, трижды привели косячные примеры кода, определили зависимость выполнения одних промисов от результатов других...
Бывает так, что необходимо параллельно или последовательно выполнять сразу множество обещаний.
20-й абзац... уже где-то почти совсем рядом...
Представьте, к примеру, сто нужно выбрать список из 1 000 пользователей GitHub, а затем сделать дополнительный запрос с ID, чтобы выбрать для каждого из них аватарки. Совсем не обязательно вы захотите дожидаться завершения этих операций со всеми пользователями в последовательности; вам нужны только все выбранные аватарки. Мы подробнее поговорим об этом ниже, когда будем обсуждать
Promise.all
.Нет, блин, опять какую-то фигню написали. Каким боком Promise.all помогает "не обязательно ... дожидаться завершения этих операций со всеми пользователями в последовательности" - решительно непонятно. Он же, вроде, и нужен для того, чтобы всенепременнейше дождаться прямо конкретно всех, без исключения?
Теперь, когда вы в общем и целом поняли, что такое промисы
Нет, все еще решительно не понимаю... Так ни разу слов "асинхронное выполнение" и примеров, хотя бы как-то с оными связанных, и не встретил. А так-то 25-й абзац уже...
Синтаксис Async/await удивительно прост при работе с промисами. Он предоставляет простой интерфейс для чтения и записи промисов, причем, таким образом, что они кажутся синхронными
Ребят, вы весь текст до этого ни разу не сказали, что промисы - асинхронные! Ну камон, вы чо, блин?
Даже если пропустить ключевое слово
Promise
, компилятор обернет вашу функцию в немедленно разрешаемый промис.Ну вот может у меня компилятор какой-то не такой, но конкретно мой компилятор ничего ни во что оборачивать не стал. Он просто вывалил мне ошибку и ничего собирать не стал. И я даже с ним согласен, я бы тоже не стал.
Конструкция
async/await
всегда возвращаетPromise
Ну нет же, право слово. async-функция всегда возвращает Promise, а await всегда дожидается, пока он зарезолвится. А что такое конструкция async/await - решительно непонятно. Например, можно объявить async функцию, и не await'ить ее. А await вполне себе используется не только в вызовах async-функций, например, можно `await somePromise` делать, это законно. И это именно потому, что никакой конструкции async/await нет, есть async(асинхронные функции), которые всегда обязаны возвращать промисы, и есть ключевое слово await, которое заставляет текущий поток исполнения дождаться, пока уже возвращенный асинхронной функцией промис зарезолвится.
Таким образом, можно трактовать возвращаемое значение функции async как
Promise
Зачем трактовать-то? Это ж он и есть!
что довольно полезно, когда нужно разрешать сразу множество асинхронных функций
Опачки, здравое зерно! Какой по счету абзац? Я что-то сбился.
Как понятно из названия,
async
сawait
всегда ходят парой.Блин, вы опять все испортили! Во-первых, вообще не понятно из названия, во-вторых, вообще не обязаны парой ходить... Можно пнуть асинхронную функцию и не дожидаться выполнения - это законно, хоть и не приветствуется (async без await). Можно просто взять готовый промис и дождаться его выполнения, что не только законно, но и в целом очень даже приветствуется (await без async).
То есть, делать
await
можно только внутри функцииasync
Ну да, await может быть использован только внутри async-функции, это да. Но ваше "то есть" предполагает, что это само собой разумеется из сказанного ранее. А это не так... Вообще никакой связи.
Тем более что запрет на использование await'а извне асинхронных функций таки стоит сильно сбоку от "async/await, ходящих парой". Если мы уж пару ищем, то, предположительно, имеем в виду связанную между собой пару из вполне конкретного async в сигнатуре функции и конкретного await'а, который резолва конкретно возвращаемого ей промиса ждет. Ну так вот, сейчас будет срыв покровов: тот await, который ждет разрешения асинхронной функции всегда и гарантировано находится снаружи относительно самой функции.
Вы что, просто из MSDN в произвольном порядке фразы дергаете и гуглотранслейтом их переводите?
const myAsync = async (): Promise<Record<string, number | string>> => { await angelMowersPromise const response = await myPaymentPromise return response }
Сразу заметно, что этот код выглядит более удобочитаемым и кажется синхронным.
Да он в контексте описанной функции и является синхронным! Ключевое слово async = асинхронное выполнение, а ключевое слово await - это оператор синронизации состояния.
Допустим, например, что у нас лег сервер
Мы должны приостановить выполнение, чтобы предотвратить обвал программы
Что, простите, мы должны предотвратить? Вы же сами говорите, он УЖЕ лег.
В качестве возвращаемого значения ожидаем массив типа
typeof
Странный какой-то тип. У меня, почему-то, есть чувство, что тип с именем typeof в TypeScript объявить нельзя...
Если какое-то исключение ускользнет, то может получиться код, плохо поддающийся отладке, либо даже может быть испорчена вся программа.
Ну, например, ровно как в вашем примере, выброшенный Exception (который не является наследником Error) вполне "испортит всю программу".
Очевидно, нам нужно выполнять эти функции в синхронной манере, но при этом параллельно, чтобы одна функция не блокировала другую.
Вот сейчас вообще не очевидно стало. Вы уж определитесь, синхронно (выполнение инструкций последовательно, в порядке описания в коде программы), или таки параллельно (очевидно, в каком-то другом порядке). Да и вообще, откуда вы это "параллельно" взяли. Асинхронно - правильный термин, параллельность TS вообще никак не гарантирует.
Или про то, чем асинхронность от параллельности отличается, тоже надо рассказать?
Как пишет Mozilla, “
Promise.all
обычно применяется после того, как было запущено множество асинхронных задач, которые должны работать конкурентно, и после того, как пообещали, каковы будут их результаты – чтобы можно было дождаться, пока все эти задачи будут завершены.”Зачем вы оговариваете Mozilla? Не могли они такую дичь написать! Это просто у вас перевод корявый.
В псевдокоде было бы что-то подобное:
Выбрать всех пользователей =>
/employee
Дождаться всех данных о пользователях. Извлечь
id
от каждого пользователя. Выбрать каждого пользователя =>/employee/{id}
Сгенерировать электронное сообщение для каждого пользователя по его имени
Псевдокод у вас тоже неправильный, последовательный (т.е. синхронный) алгоритм описан. При чем тут Promise.all?
Мы ожидаем отклик (
await
), преобразуем его вJSON
А вы это, простите, зачем делаете? (подсказка: в коде происходит не то, что вы пишете).
Самое важное, о чем здесь нужно помнить – как мы последовательно выполняли код строка за строкой внутри функции
async
с ключевым словомawait
. Мы бы получили ошибку, если бы попытались преобразовать в JSON данные, которых дождались не полностью.Божечки, что вы несете? Муля, никогда не доверяй важное дело идиотам!
Более интересен фрагмент
runAsyncFunctions
, где все асинхронные функции выполняются конкурентно.Вот это верно, он, как раз, и есть то, заради чего вся эта муть писалась. Это прям апогей, апофеоз, самая цели статьи и вишенка на торте...
Каждый
fetchEmployee Promise
конкурентно выполняется для всех сотрудников.... которая в тупую и совершенно бездарно слита буквально через абзац. Как и вся статья...
async
иawait
позволяет писать асинхронный код так, что он выглядит и действует как синхронныйПросто нет слов... Еще раз повторю: async - ключевое слово, означающее асинхронный запуск, await - инструмент синхронизации. await не позволяет писать асинхронный код, await - напрямую команда синхронного исполнения.
Честное слово:
МуляИздательский дом "Питер", не доверяйсерьезное делопубликацию статей от своего имени на хабреидиотамрерайтерам.Saiv46
03.11.2021 07:39+2Человек по делу написал, а вы его минусуете.
Конечно и оригинал такой себе, вот пример:
// Я отправляю запрос в компанию. Он синхронный // Компания обещает мне выполнить работу const angelMowersPromise = new Promise<string>((resolve, reject) => { // Обещание разрешилось спустя несколько часов setTimeout(() => { resolve('We finished mowing the lawn') }, 100000) // разрешается спустя 100 000 мс reject("We couldn't mow the lawn") })
Данный Promise будет отвергнут сразу же, не смотря на то, что спустя 100с он будет разрешён (да и писал это человек что явно не знает как работают промисы).
Julia_Konovalova
28.10.2021 12:48+1Спасибо! Было полезно прочитать ещё раз другими словами и с понятными примерами, хоть мне пока не нужен Typescript, я изучаю JS
В части Promise.All было уже не так легко понять, но всё равно спасибо ????
muturgan
29.10.2021 15:27Довольно подробное руководство по промисам и async/await. Но при чем тут тайпскрипт?
MaZaAa
Режет глаза отсутствие; в конце строк в коде. Не надо свои странные предпочтения выставлять на публику.
haymob
Typescript дописывает ; при компиляции в js, если вам не доплачиваю за ; результат будет одинаковый.
mukhindev
С каких пор это порицается? Пунктуальная нетерпимость какая-то) На всекий случай предостерегаю вас от захода в некоторые открытые репозитории и документации этих компаний https://standardjs.com/#who-uses-javascript-standard-style
qrKot
Странно видеть ссылку на JS-стайл в контексте статьи по TS...
Тем более линтер может нарисовать эти ; за вас, а публиковать в статье код, не видевший линтера, таки моветон.
muturgan
Знаю проекты на тс, в которых в стайлгайдах и настройках линтера закреплено отсутствие точек с запятой.
Приравнивать несоответствие вашим предпочтениям к недостатку, таки моветон.
ПС. Я люблю ;
Metotron0
Разве этот комментарий не является эттм самым выставлением?