Выбор языка в зависимости от типа проблемы: уроки из портирования TypeScript на Go
Чему учит нас выбор Go в качестве движка для нативного компилятора TypeScript относительно подбора инструментов под конкретные задачи, важности скорости работы инструментов и поэтапной переносимости крупных кодовых баз.
Ожидание почти минуты для автодополнения в крупном монорепозитории на TypeScript — это хорошо известная проблема, с которой сталкиваются также разработчики мобильных приложений во время индексации больших проектов в Xcode. Команда TypeScript боролась именно с этой проблемой, и способ, которым она выбрала её решение, является полезным примером принятия технологических решений на основе фактов, а не моды. В этой статье мы уходим за рамки заголовков с результатами тестов и рассматриваем инженерные аргументы, которые можно применить к собственным инструментам и приложениям, независимо от того, используете ли вы TypeScript, Swift или оба.
Что объявила Microsoft
В марте 2025 года Андерс Хейльсберг, создатель как TypeScript, так и C#, сообщил, что Microsoft разрабатывает нативную версию компилятора TypeScript и соответствующих инструментов для работы с языком. Многие предполагали, что целью будет Rust или, возможно, C++. Но на самом деле это был Go.
Оригинальная реализация, получившая кодовое название Corsa, лежит в основе TypeScript 7.0. На момент написания этого текста ожидалось выпуск примерно в середине 2026 года, поэтому перед планированием миграции обратитесь к официальному блогу TypeScript для получения актуальной информации.
Результаты тестов были впечатляющими: время загрузки кодовой базы VS Code в редактор сократилось с примерно 9,6 секунд до 1,2 секунды, использование памяти уменьшилось примерно вдвое, а Microsoft описала скорость загрузки проектов как примерно в 8 раз выше, при этом проверка типов стала в 10 раз быстрее. Если вы хотите узнать подробности о том, как эти улучшения проявляются в реальных проектах, мы уже писали об этом отдельно в статье как изменения, связанные с использованием Go, ускоряют работу без изменения вашего кода. Остальная часть этой статьи посвящена самому решению о выборе языка.
Почему Go вместо Rust
Команда искала язык наименьшего уровня, который всё ещё позволял генерировать нативный код на всех платформах, поддерживаемых TypeScript, при этом обеспечивая хорошую встроенную поддержку многопоточности. Go соответствует этому описанию.
Важную роль сыграли ещё два фактора. Во-первых, объём работы: проверка типов в крупном проекте в основном сводится к параллельной проверке множества файлов с последующим объединением результатов, и горутины очень прямо соответствуют этой схеме. Во-вторых, существующий код: компилятор представляет собой огромную базу JavaScript-кода, которая опирается на сборку мусора и довольно функциональный стиль программирования. У Go также есть сборщик мусора и похожая структура, поэтому код можно было перевести практически без изменений. Перенос кода на Rust потребовал бы значительно более сложной переработки, связанной с концепциями владения и времен жизни объектов, что стало бы большой нагрузкой для крупной команды, работающей с огромной базой кода.
Проще говоря, команда не выбрала самый модный язык. Она выбрала тот, который соответствовал её модели конкурентности и позволял инженерам эффективно работать. Это архитектурное решение, такое же, какое вы принимаете при выборе между UIKit и SwiftUI или между Combine и обычным async/await для реализации определённой функции.
Та же идея в Swift
Разработчики Swift уже сталкивались с подобной проблемой. Структурированная конкурентность была добавлена в Swift именно для выполнения ресурсоёмких операций параллельно без ущерба для корректности. Приведённый ниже фрагмент демонстрирует идею «проверить несколько файлов одновременно, затем объединить результаты» с использованием группы задач: каждый файл получает свою отдельную дочернюю задачу, а родительская задача собирает результаты по мере завершения дочерних. Обратите внимание, что фрагмент написан на Swift, хотя иллюстрирует паттерн, используемый компилятором Go с горутинами.
// Swift concurrency: parallel "type-checking" of files, similar in spirit
// to how Go's goroutines let TypeScript check many files at once
func checkFiles(_ paths: [String]) async -> [Diagnostic] {
await withTaskGroup(of: [Diagnostic].self) { group in
for path in paths {
group.addTask {
await checkSingleFile(path) // runs concurrently
}
}
var results: [Diagnostic] = []
for await diagnostics in group {
results.append(contentsOf: diagnostics)
}
return results
}
}
Стоит обратить внимание на несколько моментов. Функция withTaskGroup гарантирует, что каждая дочерняя задача завершится до возврата функции, поэтому никакая работа не ускользнет за пределы области видимости. Результаты поступают в порядке завершения, а не в порядке ввода, так что если важен порядок диагностических сообщений, их необходимо отсортировать позже. Параллелизм безопасен только потому, что каждая проверка является независимой, и именно это свойство делает проверку типов подходящей для модели Go.
Если вы используете Swift 6, вы уже сделали подобный компромисс: сначала приходится платить за строгие проверки конкурентности и безопасности, а взамен получаются более быстрая компиляция и стабильнее поведение во время выполнения. Microsoft сделала похожий выбор со своим компилятором.
Уроки, применимые к вашим собственным проектам
- Скорость работы инструментов является характеристикой продукта. Медленная сборка кода и замедленное автодополнение создают ежедневную, практически незаметную нагрузку на каждого разработчика в команде. Работа над производительностью сборочных процессов, редакторов и систем индексации так же важна, как и работа над производительностью самого выпущенного приложения.
- Пусть инструмент выбирается на основе проблемы, а не тренда. Независимо от того, выбираете ли вы MVVM, SwiftData или Combine, правильный ответ определяется тем, как на самом деле передаются данные в вашей системе, а не тем, что популярно в социальных сетях в этом месяце.
- Переписывание может происходить поэтапно. Команда, занимающаяся TypeScript, максимально точно перенесла существующий компилятор вместо того, чтобы полностью его переработать, поэтому новая версия дает такие же результаты, как и старая. Это весомый аргумент против полной перестройки архитектуры приложения за один спринт.
Когда не стоит следовать этому примеру? Если ваш текущий стек технологий не является узким местом, переход на другой фреймворк принесет только риски. Сначала проведите измерения и убедитесь, что медленная часть действительно находится в том слое, который вы планируете заменить.
Выявление предупреждающих сигналов
Симптомы, приведшие к возникновению этой проблемы, часто встречаются в любом крупном кодовом базисе: замедление процесса инференции типов, задержки в обратной связи от редактора и постоянное увеличение размеров графов сборки. Решением не всегда является использование новой платформы. Иногда достаточно внимательно изучить, что на самом деле делают ваши инструменты, и изменить ту часть, которая выполняет слишком много работы.
Основные выводы
- Go был выбран потому, что его нативная компиляция, механизм сбора мусора и конкурентность на основе горутин соответствовали как нагрузке на проверку типов, так и структуре существующего кода.
Связанные статьи
- Настоящая сложность современного JavaScript происходит от инструментов, а не от самого языка — В этой статье объясняется, как такие основные функции JavaScript, как async/await и optional chaining, упрощают код, в то время как избыточные инструменты и зависимости создают ненужную сложность.
- Внутренности переписки компилятора TypeScript 7 на Go: ускорение без изменений кода — Узнайте, как компилятор TypeScript 7 на основе технологии Go обеспечивает в 8–12 раз более быструю сборку проектов, почему такая архитектурная смена эффективна и как безопасно обновить существующие проекты.
- От одного дизайна к множеству экранов: более рациональный процесс работы с ресурсами заглушечного экрана — Почему создание заглушечных экранов занимает больше времени, чем требует их дизайн, как сделать такой экран, который будет корректно отображаться в любом соотношении сторон, и что должен делать специализированный генератор в браузере.
- Решения по обновлению до TypeScript 7: параллельные проверки, режим наблюдения и пробелы в API — Какие изменения происходят при переходе с TypeScript 6 на нативный для Go TypeScript 7, как работают новые флаги параллелизма и какие инструментальные комплексы следует использовать перед обновлением.
- От написания кода на Kotlin к управлению агентами: новая роль инженера мобильных приложений — Как работа с кодирующими агентами меняет задачи инженера Android, сосредотачивая внимание на спецификациях, контексте, архитектурных ограничениях и верификации, и какие основы становятся важнее, чем когда-либо.