Если вы пришли сюда только ради ответа и вам не интересны рассуждения - листайте вниз :)

Как все начиналось

Для начала, давайте вспомним, а как вообще ловят ошибки в js, будь то браузер или сервер. В js есть конструкция try...catch.

try {
    let data = JSON.parse('...');
} catch(err: any) {
		// если произойдет ошибка, то мы окажемся здесь
}

Это общепринятая конструкция и в большинстве языков она есть. Однако, тут есть проблема (и как окажется дальше - не единственная), эта конструкция "не будет работать" для асинхронного кода, для кода который был лет 5 назад. В те времена, в браузере использовали для Ajax запроса XMLHttpRequest.

const xhr = new XMLHttpRequest();
xhr.open('GET', 'https://api.example.com', true);
xhr.addEventListener('error', (e: ProgressEvent<XMLHttpRequestEventTarget>) => {
    // если произойдет ошибка, то мы окажемся здесь
});

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

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

import fs from 'fs';

fs.readFile('file.txt', (err, data) => {
    if (err) {
        // обработка ошибки
    }
    // если все хорошо, работаем с данными
});

Если мы посмотрим какой тип имеет переменная err, то увидим следующее:

interface ErrnoException extends Error {
    errno?: number | undefined;
    code?: string | undefined;
    path?: string | undefined;
    syscall?: string | undefined;
}

Тут действительно находится ошибка. По сути, это тот же способ, что и выше, только в этом случает мы получаем объект Error.

Через некоторое время, в Javascript появились Promise. Они, безусловно, изменили разработку на js к лучшему. Ведь никто* никто не любит городить огромные конструкции из функций обратного вызова.

fetch('https://api.example.com')
  .then(res => {
    // если все хорошо, работаем с данными
  })
  .catch(err => {
		// обработка ошибки
  });

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

try {
  const res = await fetch('https://api.example.com');
  // если все хорошо, работаем с данными
} catch(err) {
	// обработка ошибки
}

Также, конструкция try...catch позволяет ловить ошибки из нескольких промисов одновременно.

try {
  let usersRes = await fetch('https://api.example.com/users');
	let users = await usersRes.json();

  let chatsRes = await fetch('https://api.example.com/chats');
	let chats = await chatsRes.json();

  // если все хорошо, работаем с данными
} catch(err) {
	// обработка ошибки
}

Вот, замечательный вариант ловли ошибок. Любая ошибка которая возникнет внутри блока try, попадет в блок catch и мы точно её обработаем.

А точно ли обработаем?

Действительно, а правда ли, что мы обработаем ошибку, или всего лишь сделаем вид? На практике, скорее всего, возникнувшая ошибка будет просто выведена в консоль или т.п. Более того, при появлении ошибки*, интерпретатор прыгнет в блок catch , где не мы, не TypeScript не сможет вывести тип переменной, попавшей туда (пример - возврат с помощью Promise.reject), после чего, произойдет выход из функции. То есть, мы не сможем выполнить код который находится в этом же блоке, но который расположен ниже функции, внутри которой произошла ошибка. Конечно, мы можем предусмотреть такие ситуации, но сложность кода и читаемость вырастут многократно.

Как быть?

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

let [users, err] = await httpGET('https://api.example.com/users');
if (err !== null) {
	// обработка ошибки
}
// продолжаем выполнение кода

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

Пример для вызова нескольких функций возвращающих Promise.

let err: Error,
		users: User[],
		chats: Chat[];

[users, err] = await httpGET('https://api.example.com/users');
if (err !== nil) {
  // обработка ошибки
}

[chats, err] = await httpGET('https://api.example.com/chats');
if (err !== nil) {
  // обработка ошибки
}

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

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

type PairPromise<T> = Promise<[T, null] | [null, Error]>;

Определим возможные возвращаемые ошибки.

const notFoundError = new Error('NOT_FOUND');
const serviceUnavailable = new Error('SERVICE_UNAVAILABLE');

Теперь опишем нашу функцию.

const getUsers = async (): PairPromise<User[]> => {
    try {
        let res = await fetch('https://api.example.com/users');
        if (res.status === 504) {
            return Promise.resolve([null, serviceUnavailable]);
        }

        let users = await res.json() as User[];

        if (users.length === 0) {
            return Promise.resolve([null, notFoundError]);
        }

        return Promise.resolve([users, null]);
    } catch(err) {
        return Promise.resolve([null, err]);
    }
} 

Пример использования такой функции.

let [users, err] = await getUsers();
if (err !== null) {
	switch (err) {
  	case serviceUnavailable:
    	// сервис недоступен
    case notFoundError:
    	// пользователи не найдены
    default:
    	// действие при неизвестной ошибке
	}
}

Вариантов применения данного подхода обработки ошибок очень много. Мы сочетаем удобства конструкции try...catch и Error-First Callback, мы гарантированно поймаем все ошибки и сможем удобно их обработать, при необходимости. Как приятный бонус - мы не теряем типизацию. Также, мы не скованы лишь объектом Error, мы можем возвращать свои обертки и успешно их использовать, в зависимости от наших убеждений.

Очень интересно мнение сообщества на эту тему.

Комментарии (59)


  1. lair
    22.06.2022 16:28
    +1

    let [users, err] = await getUsers();
    if (err !== null) {
        switch (err) {
        case serviceUnavailable:
            // сервис недоступен
        case notFoundError:
            // пользователи не найдены
        default:
            // действие при неизвестной ошибке
        }
    }

    мы гарантированно поймаем все ошибки

    … и что гарантирует, что возвращенная ошибка действительно будет обработана, а не будет так, что вызывающий код просто ее отбросит и вернет users?


    1. halfcupgreentea
      22.06.2022 16:47
      +1

      Тут может помочь TS: пока не будет проверен тип err, тип users будет Users | null Например


    1. asvxyz Автор
      22.06.2022 17:08

      Действительно, ничто не может заставить разработчика обязательно обрабатывать все ошибки. Более того, такой способ позволяет не извлекать ошибку вообще. Но, такой код стимулирует разработчика озаботиться обработкой ошибки. В случае с try…catch, по опыту, почти все кейсы будут опущены…(


      1. lair
        22.06.2022 17:09
        +1

        Но, такой код стимулирует разработчика озаботиться обработкой ошибки.

        Не понимаю, каким образом.


        В случае с try…catch, по опыту, почти все кейсы будут опущены…

        Что значит "опущены"? В случае с эксепшном, если нет catch, разве ошибка не будет брошена в вызывающем коде?


        1. asvxyz Автор
          22.06.2022 17:19
          -1

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


          1. lair
            22.06.2022 18:02
            +3

            Такой подход позволят узнать об ошибке «не отходя от кассы», как говориться.

            Позволяет узнать — да. Но как это стимулирует их обрабатывать?


            Более того, это банально лучше читается

            А точно это лучше читается? Я вот предпочитаю видеть в первую очередь обработку основного сценария, а не ошибок.


            1. asvxyz Автор
              22.06.2022 18:55
              -1

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


              1. lair
                22.06.2022 18:58
                +4

                Обработка ошибок должна быть неотъемлемой частью сценария

                Почему обработка ошибок вида "у меня тут сеть недоступна" должна быть неотъемлемой частью бизнес-сценария?


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


                1. alexesDev
                  23.06.2022 12:05
                  -1

                  Только проблема в раскрытии имплементации бизнес логики... если планируется обрабатывать "у меня тут сеть недоступна" не как базовый Exception, то стоит делать обертку и это будет неотъемлемой частью логики. Убирать неявное - хорошая практика.


                  1. lair
                    23.06.2022 14:32

                    Если планируется — то будет. А если не планируется, то не будет, и можно просто не писать лишнего кода.


  1. dpereverza
    22.06.2022 16:38
    +5

    Вариант с Go'шной обработкой ошибок, заставляет нас проверить на ошибки результат. Но с той же легкостью мы можем этого и не делать)

    Ленивые разработчики будут писать что-то типа
    [chats] = await httpGET('https://api.example.com/chats');

    Но на CodeReview такие косяки будет заметнее, и это хорошо.
    А вот что плохо, так это загрязнение кода от этих постоянных проверок.

    Есть ФП подход к обработке ошибок, он сложнее, но если привыкнуть то гуд.
    https://habr.com/ru/post/457098/


    1. asvxyz Автор
      22.06.2022 17:28
      -2

      Да, такой подход позволяет опускать ошибки (как и в го, кстати), но позволяет меньше отвлекаться на этапе прототипирования.

      По поводу «плохо» - если может возникнуть ошибка, то она возникнет) Это первое. Если мы не хотим проверять ошибки(загрязнять код(c)), то можно как в случае с try…catch замести все под коврик и просто вывести пользователю “sorry…”


  1. SergeiMinaev
    22.06.2022 17:10

    Для начала, давайте вспомним, а как вообще ловят ошибки в js, будь то браузер или сервер. В js есть конструкция try...catch.

    Несмотря на то, что даже в MDN фигурирует именно слово "ошибки", мне кажется более уместным всё-таки называть это исключениями. В англ. оригинале как раз используется слово "exception".

    UPD: Имею в виду, что ошибки бывают восстановимыми и невосстановимыми. Во втором случае, если не делать try/catch, то программа падает и это называется исключением. А в конце этой статьи как раз приводится пример работы с восстановимыми ошибками, что в последнее время считается более правильным.


    1. asvxyz Автор
      22.06.2022 17:21
      -2

      На самом деле, в реальном (более низком уровне) мире, граница между ошибкой и исключением очень размыта.


  1. monochromer
    22.06.2022 17:32
    +2

    Позанудствую.

    if (users.length === 0) {
      return Promise.resolve([null, notFoundError]);
    }

    Нулевая длина списка вряд ли должна описываться ошибкой notFoundError. Ведь коллекция `users` есть. Если бы пользователь пошёл по пути `/users/1/`, а пользователя c `id = 1` нет, то тогда можно отдать notFoundError.

    Из async-функции разве не достаточно возвращать просто массив `[data, error]` без оборачивания в Promise.resolve ?


    1. superconductor
      22.06.2022 18:17

      Достаточно


    1. asvxyz Автор
      22.06.2022 18:48

      Так написано для большей наглядности. Да и мне так больше нравится (субъективно).

      Что касается ‘not found’ , действительно, это притянутый за уши пример, тут может быть любая другая ошибка.


  1. return
    22.06.2022 18:29
    +5

    Далеко не всегда нужно обрабатывать ошибки и очень часто ошибка, брошенная через throw вполне себе может привести к 500 ошибке и записана куда-нибудь в лог и это будет правильно.

    Все удобство в try..catch в том, чтобы отловить только те ошибки, на которые ты должен как-то специфически отреагировать. А остальные - ну а как ты их нормально обработаешь? Пусть себе ловит какой-то общий обработчик, который запишет в лог, а юзеру скажет сорян. Увидел в логе необработанную ошибку, понял, что такой ситуации можно избежать — делаешь catch и именно ее и ловишь, как-то так ????‍♂️


    1. asvxyz Автор
      22.06.2022 18:51
      -1

      Для описанного вами случая можно использовать “switch default”


      1. return
        22.06.2022 18:53
        +4

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


        1. asvxyz Автор
          22.06.2022 19:06
          -2

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


          1. return
            22.06.2022 19:15
            +3

            Прекрасно представляю, если жить без сервиса можно - вполне себе ок обработать ошибки от него. Если важен и без него никак (обычно это как раз такие) - то сорян, никакой обработки делать не нужно, пусть ловится глобально.

            Все это надо держать в балансе, заставлять разработчика проверять ошибки всегда — неправильно


          1. lair
            23.06.2022 01:55
            +3

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

            … а обычный catch не позволяет?


            1. asvxyz Автор
              23.06.2022 13:43

              Позволяет!

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


              1. lair
                23.06.2022 14:31
                +1

                Но идея такого подхода, фактически, не заменить, но дополнить стандартный подход

                Когда в системе два подхода к обработке ошибок, программисты начинают их путать.


                1. asvxyz Автор
                  23.06.2022 14:41
                  -1

                  Я выше приводил примеры такого как ошибки обрабатываются в случает XMLHttpRequest и readFile, в первом случае - подписка на событие ошибки, второй - ошибка передаётся первым аргументом в колбеке. Вообще забавна такая реакция сообщества, такое ощущение, что никто не знает про вышеописанные способы/приёмы. Вариант который предлагаю я, по сути, синтаксический сахар для ErrorFirstCallback. С трудом верится, что люди не знают такие вещи, видимо, такие люди просто более активны в комментариях…


                  1. lair
                    23.06.2022 14:44

                    Я выше приводил примеры такого как ошибки обрабатываются в случает XMLHttpRequest и readFile, в первом случае — подписка на событие ошибки, второй — ошибка передаётся первым аргументом в колбеке.

                    Это называется "легаси". И это как раз и неудобно, что для обработки ошибок используются разные подходы.


                    1. asvxyz Автор
                      23.06.2022 14:52

                      Подход у XMLHttpRequest - показывает, что жизнь может быть чуть сложнее. С readFile, действительно, сейчас есть возможность использовать вариант с промисом. Но старый вариант позволял выполнить определенный код не теряя "контекст".


                      1. lair
                        23.06.2022 15:04

                        Подход у XMLHttpRequest — показывает, что жизнь может быть чуть сложнее.

                        Так это неудобно же. Наличие нескольких разных способов обработать ошибку — неудобно (потому что лишает код консистентности).


                      1. asvxyz Автор
                        23.06.2022 15:12

                        И да, и нет. XMLHttpRequest имеет много разных событий: error, abort, timeout... Это как раз пример того, что ошибок может быть много и разных, и специфика этих ошибок в том, что их нужно по разному обрабатывать. Нас же не смущает, то, что мы используем колбеки для подписки на события. Timeout, abort, error - тоже события. С другой стороны, с "религиозной" точки зрения - "Это другое!".


                      1. lair
                        23.06.2022 15:18
                        +1

                        Это как раз пример того, что ошибок может быть много и разных, и специфика этих ошибок в том, что их нужно по разному обрабатывать.

                        Нет, это пример того, что бывают события, которые отражают ошибку.


                        специфика этих ошибок в том, что их нужно по разному обрабатывать

                        Ну так ошибки и в catch можно по-разному обрабатывать.


                        Нас же не смущает, то, что мы используем колбеки для подписки на события.

                        Если эти события нужны только для поддержания асинхронии — смущает.


                        Надо разделять ошибку как результат процесса (тогда ошибок больше возникнуть не может, можно ее вернуть из метода, как в вашем примере, или бросить как в try-catch), или ошибку как событие в процессе (они могут возникнуть еще потом, процесс не остановился, тогда ее нельзя вернуть, не годится ни try-catch, ни ваш подход).


                      1. asvxyz Автор
                        23.06.2022 15:34
                        -2

                        Ну так ошибки и в catch можно по-разному обрабатывать.

                        Да, но тут ты на уровне интерфейса (названия событий) понимаешь какие могут быть ошибки.

                        Возвращаемся к try...catch

                        try {
                          ...
                        	let data = await getDataFromCache();
                        	if (!data) {
                            // если в кэше этих данных нет
                          	data = await getDataFromDB(); 
                          }
                        	...
                        } catch(e) {
                        	// здесь может быть ошибка из кэша или db
                        }

                        Иначе

                        let data, err;
                        [data, err] = await getDataFromCache();
                        if (err !== null) {
                        	// например, кэш не успел подняться после 
                          // перезагрузки, это не повод идти в catch, 
                          // можно побробовать взять из базы или что то еще 
                        }
                        

                        Да, можно использовать только try...catch , но...

                        try {
                            let x = 5;
                            throw new Error();
                        } catch(e) {
                            console.log(x);
                        		// Uncaught ReferenceError: x is not defined
                        }


                      1. mayorovp
                        23.06.2022 15:38
                        +1

                        В вашем первом примере общая обработка для двух источников ошибки — это преимущество. А если надо разделить обработку — никто не мешает написать два разных блока try…catch


                        Что же до ReferenceError — да, не вполне удобно, но способ обхода общеизвестен. Всего-то надо объявить переменную блоком выше.


                      1. asvxyz Автор
                        23.06.2022 15:39
                        -2

                        То есть, все таки, лишний код можно писать, но только тогда, когда его ты сам благословил?


                      1. mayorovp
                        23.06.2022 15:42

                        В каком смысле "благословил"?


                      1. asvxyz Автор
                        23.06.2022 15:51

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

                        Благословил - я имел ввиду, что когда другие пишут 'лишний' код, это очень плохо, нужно минусовать. Если я пишу - это правильно, я знаю как лучше.

                        Вот так, грубо говоря.


                      1. mayorovp
                        23.06.2022 15:54

                        Повторюсь:


                        общая обработка для двух источников ошибки — это преимущество

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


                      1. asvxyz Автор
                        23.06.2022 15:57
                        -2

                        Чаще / реже, не значит, что нужно использовать подход обозначенный в статье, так же как и не значит, что не нужно использовать общий подход. Это просто еще один способ (по моему мнению более прогрессивный), не более того. Ну и "субъективно" для меня, читаемость такого кода лучше


                      1. lair
                        23.06.2022 15:38

                        Да, но тут ты на уровне интерфейса (названия событий) понимаешь какие могут быть ошибки.

                        Нет. Я понимаю, какие могут быть события. А все ошибки — это все так же error. Вы можете сказать, какие ошибки могут там быть?


                        Да, можно использовать только try...catch, но...

                        … а что вам мешает использовать больше одного try-catch?


                      1. asvxyz Автор
                        23.06.2022 15:40
                        -2

                        читай мой коммент выше


                      1. lair
                        23.06.2022 15:41

                        Не отвечает на мои вопросы.


                      1. asvxyz Автор
                        23.06.2022 15:55
                        -2

                        Разная специфика ошибок, требующая по-разному реагировать.

                        Кажется, что я столкнулся с 'религиозной' догмой...


                      1. mayorovp
                        23.06.2022 16:04

                        На религиозную догму больше похож как раз ваш вариант.


                      1. asvxyz Автор
                        23.06.2022 16:07

                        Серьезно? Я говорю, что можно применять разные подходы в зависимости от ситуации. Я не ограничиваюсь лишь своим подходом, меня не пугает ни try...catch, не onError, ни Error-First-Callback. Пока мне не слили рейтинг с кармой я не минусовал людей с другой точкой зрения, а общался. Но в тоже время, меня решили заминусовать. Так где же у меня догма?


                      1. lair
                        23.06.2022 16:12

                        Разная специфика ошибок, требующая по-разному реагировать.

                        Так как раз это прекрасно покрывается разными try-catch и дифференцированной обработкой ошибок в них. В чем проблема?


                      1. asvxyz Автор
                        23.06.2022 16:23

                        Как я писал выше, способ описанный в статье, позволяет обрабатывать ошибки по мере их поступления (возможного). Что касается catch, действительно, он ловит все ошибки и мы спокойно их все разгребем. Проблема лишь в том, что мы все это делаем в одной куче.


                      1. lair
                        23.06.2022 16:28

                        Как я писал выше, способ описанный в статье, позволяет обрабатывать ошибки по мере их поступления (возможного).

                        А я думал, он их обрабатывает после возврата из метода?...


                        Проблема лишь в том, что мы все это делаем в одной куче.

                        Ну так не делайте в одной куче, делайте столько try-catch, сколько вам надо.


                      1. asvxyz Автор
                        23.06.2022 16:32

                        Ну вот мое мнение, как раз в том и состоит, что по отдельности обрабатывать ошибки удобнее (по моему мнению, два try-catch в одном блоке уже перебор ) способом из статьи, если нужно все вместе - catch


                      1. lair
                        23.06.2022 17:31

                        по моему мнению, два try-catch в одном блоке уже перебор

                        Но почему?


                        А еще понимаете ли в чем дело, вызываемый код не знает, как его будут использовать. И как ему возвращать ошибки?..


                      1. asvxyz Автор
                        23.06.2022 18:17
                        -2

                        Естественно, что не знает. Точно также, как и код возвращающий объект типа.

                        type Result = {
                            status: boolean;
                            message: string;
                        }


                      1. lair
                        23.06.2022 18:18

                        Так и как же вы предлагаете вызываемому коду возвращать ошибки, учитывая, что у вас две разных парадгмы в зависимости от того, как код вызывается?


                      1. asvxyz Автор
                        23.06.2022 16:36
                        -2

                        Кстати, если попытаться отвлечься от войны, можно увидеть, что наш разговор превратился в спор похожий на ситуацию "табуляция против пробела"... =)


                      1. lair
                        23.06.2022 17:36

                        И вот я вам по личному опыту могу сказать, что проект в котором в одном файле табы, а в другом — пробелы, намного хуже того, где только табы, или только пробелы (и не важно, какой стиль мне больше нравится).


                      1. asvxyz Автор
                        23.06.2022 18:11
                        -2

                        Как жаль, что я уже не могу поставить вам плюс.


                      1. asvxyz Автор
                        23.06.2022 18:19
                        -2

                        @lair, друг, извини, что я так сильно тебя обидел, и что из-за этого тебе пришлось лепить минусы на все мои сообщения.


                      1. dopusteam
                        24.06.2022 21:10

                        Предположу, что лепит не он


  1. Deka87
    22.06.2022 21:01
    +3

    В заголовке указан JavaScript, а в примерах TypeScript. Наверное, не стоит подразумевать, что все JavaScript разработчики пишут на последнем. Тем более, что эту статью могут найти через много лет, когда TS может быть уже не таким популярным, и это введёт в заблуждение.


    1. Ilusha
      23.06.2022 00:53
      -1

      Думаю, что стоит идти в ногу со временем. И стоит заниматься популяризацией TS. Тем более сейчас это происходит настолько органично, что не вызывает ни отторжения.

      А через много лет кто-нибудь напишет новую статью.


      1. Deka87
        23.06.2022 01:13
        +5

        Вероятно также думали разработчики, которые писали на jQuery в своё время (не смотря на всю разницу между jQuery и TS). Предлагаю все таки придерживаться каких-то правил и указывать TS, если речь идёт о TS, и JS, если речь идёт о чистом JS. Иначе многие начинающие разработчики могут не понять неожиданный синтакс.