2026 г.

Современный JavaScript и TypeScript

Проект ЦИТадель

JavaScript появился как язык сценариев для веб-страниц, но теперь исполняется также на серверах, в редакторах и в утилитах командной строки. За это время в стандарт вошли блочные объявления, модули, промисы и другой синтаксис, которого не было в языке 2000-х годов. Статья предназначена для читателя, знакомого с программированием, но давно не работавшего с JavaScript: она объясняет современный основной набор языка, асинхронность, место TypeScript и устройство инструментальной экосистемы. Практикум связывает тему с предыдущей статьёй — в нём предстоит написать типизированный клиент к веб-API.

1. Язык повзрослел

Крупное обновление ECMAScript 2015 стало началом ежегодных выпусков стандарта. Старые конструкции сохранились ради совместимости с существующими страницами, однако новый код обычно опирается на другой набор средств:

  • Блочные объявления. let и const принадлежат ближайшему блоку, в отличие от функциональной области var. Их связывания создаются при входе в область, но недоступны до выполнения объявления — это называют временной мёртвой зоной. const запрещает переприсвоить само связывание, но не делает объект неизменяемым.
  • Модули. import и export задают связи между файлами на уровне языка. Статические импорты позволяют заранее построить значительную часть графа зависимостей; динамический import() загружает модуль по требованию. Удаление неиспользуемого кода возможно только при корректном анализе побочных эффектов и не гарантируется одним наличием модулей.
  • Работа с данными. Деструктуризация, синтаксис spread/rest, шаблонные строки, опциональная цепочка a?.b?.c и оператор ?? сокращают служебный код. Важно различать ??, реагирующий только на null и undefined, и ||, для которого ложными являются также 0, пустая строка и false.
  • Функции, коллекции и протоколы. У стрелочной функции нет собственного this, поэтому она удобна для коллбэков, которым нужен this внешней области, но не заменяет всякую обычную функцию. Классы дают декларативный синтаксис поверх прототипной модели. Map и Set, итераторы и генераторы позволяют явно выразить словари, множества и последовательности.

Совместимость остаётся важным ограничением эволюции языка, но она не означает, что любое прежнее поведение закреплено навсегда: спецификация исправляет ошибки и иногда уточняет семантику. Целевые среды проекта определяют, какие возможности можно доставить непосредственно, а какие требуют преобразования или полифила.

2. Асинхронность: коллбэки, промисы и async/await

Как показано в статье «Как устроен браузер», выполнение сценария связано с циклом событий, очередями задач и микрозадач. Это не означает, что весь браузер однопоточен: речь идёт о последовательном выполнении кода одним агентом JavaScript. Способы описать продолжение асинхронной операции развивались поэтапно:

  • Коллбэки. Продолжение передаётся функции, начинающей операцию. Для одной операции этого достаточно, но несколько зависимых шагов порождают вложенность, а композицию, обработку ошибок и завершение приходится согласовывать вручную.
  • Промисы. Promise представляет будущий результат и позволяет передавать его и соединять операции цепочками then/catch. Promise.all ждёт успешного завершения всех переданных обещаний и отклоняется после первого отказа; allSettled собирает все исходы; race принимает первый завершившийся исход; any — первое успешное значение. Эти комбинаторы не отменяют оставшиеся операции и сами по себе не создают параллельное вычисление: работа начинается там, где созданы соответствующие обещания.
  • async/await. Асинхронная функция всегда возвращает обещание. await приостанавливает только выполнение этой функции до получения результата, после чего её продолжение ставится в очередь; поток при этом не блокируется. try/catch обрабатывает отказ ожидаемого обещания. Ожидание внутри обычного цикла даёт последовательность, а предварительный запуск операций с последующим Promise.all — конкурентное ожидание. Конкурентность следует ограничивать, если сервис, сеть или память не рассчитаны на весь набор сразу.

Промис не предоставляет отмену сам по себе. Веб-API могут принять AbortSignal; например, fetch прекращает клиентское ожидание и чтение ответа после отмены, но это ещё не доказывает, что сервер отменил обработку. Тайм-аут, повтор, задержка с джиттером и идемпотентность остаются частью протокола операции. Эти правила разобраны в статье «Надёжность поверх ненадёжной сети».

3. TypeScript: типы для выросшего JavaScript

В небольшом JavaScript-модуле форму значения часто видно из соседнего кода и тестов. С ростом числа модулей и участников становится труднее проследить все допустимые входы функции и безопасно переименовать поле. TypeScript добавляет к JavaScript статическую проверку типов и инструменты для навигации и рефакторинга. Для его применения важны три свойства:

  • Сведения о типах стираются. После проверки аннотации не остаются в исполняемом JavaScript, и TypeScript не вставляет проверку входных данных. Утверждение value as Book сообщает что-то компилятору, но ничего не проверяет. Ответ API сначала следует рассматривать как unknown и валидировать по схеме из статьи «JSON и дизайн API».
  • Совместимость в основном структурна. Значение подходит типу, если имеет требуемые члены совместимых типов, даже без явного объявления implements. Это соответствует распространённым объектным интерфейсам JavaScript, но система намеренно допускает некоторые непроверяемые конструкции. any, утверждения типов и неточные декларации библиотек могут обойти защиту компилятора.
  • Типы выводятся, а проверку можно вводить постепенно. Компилятор восстанавливает многие типы из инициализаторов, условий и возвращаемых значений, поэтому аннотация нужна не у каждой переменной. Существующий проект можно начать проверять в файлах JavaScript через checkJs и JSDoc, затем переводить по файлам и усиливать настройки. Для нового проекта разумной исходной точкой служит strict, объединяющий семейство строгих проверок.

Объединения, литеральные типы, дженерики, keyof, условные и отображаемые типы позволяют выводить один контракт из другого. Проверки typeof, in и поля-дискриминаторы сужают объединение внутри ветви. Такая выразительность полезна, пока контракт остаётся понятен читателю и сообщения компилятора помогают исправить код. Сложный вычисляемый тип стоит заменить более явным, если его труднее сопровождать, чем описываемую им программу.

4. Среды и экосистема: сила и её цена

JavaScript не привязан к браузеру. В Node.js обратные вызовы одного цикла событий обычно выполняют JavaScript последовательно, а неблокирующий ввод-вывод и пул рабочих потоков позволяют обслуживать другие операции во время ожидания. Долгое синхронное вычисление задерживает все задачи данного цикла. Для вычислений можно использовать worker_threads, пул отдельных процессов или внешний сервис; рабочие потоки полезны прежде всего для нагрузки на процессор, а не как замена штатному асинхронному вводу-выводу.

Пакеты ускоряют разработку, но каждая прямая и транзитивная зависимость входит в цепочку поставки. Перед добавлением оценивают необходимость, сопровождение, разрешения установочных сценариев и размер дерева зависимостей. Lock-файл сохраняют в репозитории и используют воспроизводимую установку в конвейере; обновления проверяют тестами и просматривают отдельно от прикладных изменений. Аудит известных уязвимостей, подписи реестра и сведения о происхождении пакета дают дополнительные сигналы, но не доказывают безопасность исходного кода.

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

5. Практикум: типизированный клиент к нашему API

Сквозная задача — клиент к библиотечному API из статьи о JSON и дизайне API. Нужны среда с поддержкой fetch, TypeScript и редактор, показывающий результаты проверки.

Задание 1. Настройка проекта. Включите strict и запретите публикацию результата при ошибке проверки. Опишите типы Book, Reader и Loan по схемам OpenAPI. Сопоставьте строковые идентификаторы, календарные даты, моменты времени, необязательные поля и null с решениями первой статьи.

Задание 2. Асинхронный клиент. Реализуйте функции на fetch и async/await: получение книги, чтение списка с курсорной пагинацией и создание выдачи с ключом идемпотентности. Введите общий тип страницы результатов. Добавьте сигнал отмены и ограничение времени ожидания. Повторяйте только те запросы и ответы, для которых контракт допускает повтор: учитывайте метод, ключ идемпотентности, Retry-After, число попыток и случайную составляющую задержки.

Задание 3. Граница с внешним миром. Пусть заглушка вернёт неверный тип одного поля. Сначала скройте проблему утверждением as Book и проследите, где проявится ошибка. Затем примите результат как unknown, проверьте его по схеме и сформируйте сообщение с путём к неверному полю. Типизированным объект становится только результат успешной проверки.

Задание 4. Последовательно и конкурентно. Загрузите двадцать книг тремя способами: с await в цикле, одним Promise.all и пулом с небольшим пределом одновременных запросов. Замерьте время и число незавершённых запросов. Добавьте один отказ и проверьте, что происходит с остальными операциями после отклонения Promise.all.

Задание 5*. Сужение типов. Опишите прикладной результат как объединение «успех или ожидаемая ошибка» с полем-дискриминатором. Реализуйте полный разбор вариантов и используйте never, чтобы компилятор обнаруживал забытый вариант. Сравните это представление с исключением для транспортного сбоя или нарушения инварианта.

Итоги

  • Современный JavaScript предоставляет блочные объявления, модули, удобный синтаксис работы с данными и протоколы коллекций, сохраняя прототипную модель и значительную обратную совместимость.
  • Промисы представляют будущий результат, а async/await дают последовательную запись продолжений. Promise.all объединяет уже начатые операции, не запускает и не отменяет их.
  • TypeScript проверяет структурные контракты до исполнения. Сведения о типах стираются, поэтому внешние данные требуют отдельной проверки.
  • Цикл событий подходит для конкурентного ввода-вывода, если код не занимает его долгой синхронной работой. Нагрузку на процессор изолируют в рабочих потоках или процессах.
  • Зависимости и стадии сборки имеют стоимость. Lock-файл, воспроизводимая установка, проверка обновлений и минимальный измеримый конвейер уменьшают, но не устраняют риски.

Литература

  1. ECMAScript Language Specification — нормативная семантика объявлений, модулей, обещаний и асинхронных функций.
  2. TypeScript Handbook: The Basics и Type Compatibility — стирание типов, строгий режим и структурная совместимость.
  3. Node.js: Don't Block the Event Loop и Worker Threads — границы цикла событий и рабочие потоки.
  4. Документация npm audit — аудит известных уязвимостей, подписей и сведений о происхождении пакетов.
  5. Оглавление раздела «Веб-технологии»; другие статьи цикла: «Как устроен браузер», «JSON и дизайн API», «Как устроен фронтенд-фреймворк» и «Веб-производительность».
404 Not Found

404 Not Found


nginx/1.24.0 (Ubuntu)

Связь с редакцией