Вибір мови залежно від типу проблеми: уроки з портування 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, спрощують код, тоді як зайві інструменти та залежності створюють непотрібну складність.
- Inside TypeScript 7's Go Rewrite: Speed Gains Without Code Changes — Дізнайтеся, як компілятор TypeScript 7 на основі Go забезпечує у 8–12 разів швидшу компіляцію без змін у коді, чому така архітектурна зміна ефективна та як безпечно оновлювати існуючі проекти.
- From One Design to Many Screens: A Saner Splash Screen Asset Workflow — Чому екрани завантаження займають більше часу, ніж це диктує їхній дизайн, як створити екран завантаження, який підходить до будь-якого співвідношення сторін, та що має робити спеціалізований генератор у браузері.
- Рішення щодо оновлення до TypeScript 7: паралельні перевірки, режим спостереження та прогалини API — Які зміни відбуваються при переході від TypeScript 6 до нативного для Go TypeScript 7, як працюють нові флаги паралелізму та які інструментальні комплекси варто залишити як є перед оновленням.
- Від написання коду на Kotlin до керування агентами: нова роль інженера мобільних додатків — Як кодування агентів змінює роботу інженера Android у напрямку формулювання специфікацій, врахування контексту, архітектурних обмежень та перевірки, та які основи стають важливішими, ніж будь-коли.