Choisir un langage en fonction de la nature du problème : leçons tirées de la portée Go de TypeScript
Ce que le choix de Go comme compilateur natif pour TypeScript nous apprend sur l’adaptation des outils aux charges de travail, l’importance de la vitesse d’exécution des outils et le transfert progressif de grands bases de code.
Attendre près d’une minute pour que l’autocomplétion fonctionne dans un grand monorepo TypeScript est une expérience douloureuse bien connue, et c’est la même difficulté que ressentent les développeurs mobiles lorsque Xcode indexe un projet volumineux. L’équipe de TypeScript a lutté contre ce problème précis, et la manière dont elle a choisi de le résoudre constitue un exemple utile pour prendre des décisions technologiques en se basant sur des preuves plutôt que sur la mode. Cet article va au-delà des titres faisant état de performances et met en évidence les raisons techniques que vous pouvez appliquer à vos propres outils et applications, que vous utilisiez TypeScript, Swift ou les deux.
Ce que Microsoft a annoncé
En mars 2025, Anders Hejlsberg, créateur à la fois de TypeScript et de C#, a annoncé que Microsoft développait une version native du compilateur TypeScript ainsi que de ses outils linguistiques. Beaucoup pensaient que la cible serait Rust, ou peut-être C++. En réalité, il s’agissait de Go.
L’implémentation native, surnommée Corsa, constitue la base de TypeScript 7.0. Au moment de la rédaction de ce texte, les prévisions indiquaient une sortie vers le milieu de 2026 ; veuillez donc consulter le blog officiel de TypeScript pour connaître l’état actuel avant de planifier une migration.
Les chiffres rapportés étaient impressionnants : le temps de chargement du codebase de VS Code dans l’éditeur est passé d’environ 9,6 secondes à 1,2 seconde, l’utilisation de la mémoire a été réduite d’environ moitié, et Microsoft a déclaré que le chargement des projets était globalement environ 8 fois plus rapide, avec un contrôle de types jusqu’à 10 fois plus rapide. Si vous souhaitez en savoir davantage sur la manière dont ces améliorations se traduisent dans des projets réels, nous en avons traité séparément dans ce que changent les réécritures en Go sans modifier votre code. Le reste de cet article porte sur la décision elle-même.
Pourquoi Go plutôt que Rust
L’équipe cherchait le langage de niveau le plus bas qui puisse encore générer du code natif sur toutes les plateformes prises en charge par TypeScript, tout en offrant une bonne gestion concurrentielle intégrée. Go correspond à cette description.
Deux autres facteurs étaient importants. Premièrement, la charge de travail : le contrôle de type d’un projet volumineux consiste principalement à vérifier de nombreux fichiers en parallèle avant de combiner les résultats, et les goroutines s’adaptent très directement à ce modèle. Deuxièmement, le code existant : le compilateur est une vaste base de code JavaScript qui repose sur la collecte de déchets et un style plutôt fonctionnel. Go dispose également d’un collecteur de déchets et d’une structure similaire, ce qui permet une traduction très fidèle du code. Le passer à Rust aurait exigé une refonte beaucoup plus importante concernant la gestion de la propriété et des durées de vie, ce qui représenterait un coût élevé pour une grande équipe devant transférer une base de code immense.
En termes simples, l’équipe n’a pas choisi le langage le plus à la mode. Elle a opté pour celui qui correspondait à son modèle de concurrence et permettait à ses ingénieurs de rester productifs. Il s’agit d’un jugement architectural, du même type que celui que l’on prend en choisissant entre UIKit et SwiftUI, ou entre Combine et le simple async/await pour une fonctionnalité donnée.
La même idée en Swift
Les développeurs Swift ont déjà rencontré ce type de problème. La concurrence structurée a été ajoutée à Swift précisément pour exécuter des tâches coûteuses en parallèle sans compromettre la correction. Le fragment suivant illustre l’idée de « vérifier plusieurs fichiers en même temps, puis les fusionner » à l’aide d’un groupe de tâches : chaque fichier dispose de sa propre tâche enfant, et le parent collecte les résultats au fur et à mesure que les tâches se terminent. Notez que ce fragment est écrit en Swift, même s’il montre le schéma utilisé par le compilateur Go avec les goroutines.
// 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
}
}
Quelques points méritent d’être notés. withTaskGroup garantit que chaque tâche enfant s’achève avant que la fonction ne retourne, de sorte qu’aucun travail ne sort du cadre défini. Les résultats arrivent dans l’ordre d’achèvement et non dans l’ordre d’entrée ; par conséquent, si l’ordre des diagnostics est important, vous devez les trier ultérieurement. De plus, le parallélisme n’est sécurisé que parce que chaque vérification est indépendante, ce qui correspond précisément à la propriété qui rend le contrôle de type adapté au modèle de Go.
Si vous utilisez Swift 6, vous avez déjà fait ce type d’échange : vous payez à l’avance pour obtenir des vérifications strictes de concurrence et de sécurité, ce qui se traduit plus tard par des compilations plus rapides et un comportement en temps de exécution plus stable. Microsoft a fait un pari similaire avec son compilateur.
Leçons applicables à vos propres projets
- La vitesse d’exécution des outils fait partie des fonctionnalités du produit. Des processus de compilation lents et un complétion automatique inefficace imposent une charge quotidienne, souvent invisible, à chaque développeur au sein d’une équipe. Les améliorations de performance concernant les pipelines de compilation, les éditeurs et l’indexation sont tout aussi importantes que celles apportées à l’application finale.
- Laissez le problème, et non la tendance, choisir l’outil. Que vous optiez pour MVVM, SwiftData ou Combine, la bonne solution découle du flux réel des données dans votre système, et non de ce qui est populaire sur les réseaux sociaux ce mois-ci.
- Les réécritures peuvent être progressives. L’équipe de TypeScript a porté le compilateur existant avec la plus grande fidélité possible plutôt que de le redessiner, de sorte que la nouvelle version produit les mêmes résultats que l’ancienne. C’est un argument solide contre la reconstruction complète d’une architecture d’application en une seule itération.
Quand ne devriez-vous pas suivre cet exemple ? Si votre stack actuelle n’est pas le goulot d’étranglement, l’adoption d’une nouvelle technologie ne vous apporte que des risques. Mesurez d’abord et assurez-vous que la partie lente se trouve bien dans la couche que vous envisagez de remplacer.
Reconnaître les signaux d’alerte
Les symptômes qui ont conduit à ce projet apparaissent généralement dans toute base de code qui devient volumineuse : une inférence de type qui ralentit, des retours de l’éditeur qui sont en retard, et des graphes de compilation qui continuent de s’étendre. La solution n’est pas toujours un nouveau framework. Parfois, il s’agit d’examiner de près ce que vos outils font réellement en arrière-plan et de modifier la partie qui effectue trop de travail.
Points clés
- Go a été choisi parce que sa compilation native, sa collecte de déchets et sa concurrence basée sur les goroutines correspondaient à la fois au volume de travail lié aux vérifications de type et à la structure du code existant.
Lectures complémentaires
- La vraie complexité de Modern JavaScript provient des outils et non du langage — Cet article explique comment les fonctionnalités de base de JavaScript, telles que async/await et le chaining optionnel, simplifient le code, tandis qu’un excès d’outils et de dépendances crée une complexité inutile.
- À l’intérieur de la réécriture en Go de TypeScript 7 : des gains de vitesse sans modifications de code — Découvrez comment le compilateur basé sur Go de TypeScript 7 permet des builds 8 à 12 fois plus rapides, pourquoi ce changement d’architecture fonctionne, et comment mettre à niveau en toute sécurité des projets existants.
- De un design à plusieurs écrans : un flux de travail plus rationnel pour les éléments d’accueil — Pourquoi les écrans d’accueil prennent plus de temps que leur conception ne le justifie, comment créer un écran d’accueil adapté à tous les rapports d’aspect, et quelles fonctions un générateur basé sur le navigateur ciblé devrait avoir.
- Décisions de mise à jour de TypeScript 7 : checkeurs parallèles, mode surveillance et fentes API — Quelles sont les changements lors du passage de TypeScript 6 au TypeScript 7 natif de Go, comment fonctionnent les nouveaux indicateurs de parallélisme, et quels outils devraient attendre avant de mettre à jour.
- De l’écriture de Kotlin au directionnement des agents : le nouveau rôle de l’ingénieur mobile — Comment la programmation d’agents transforme le travail de l’ingénieur Android en fonction des spécifications, du contexte, des contraintes architecturales et de la vérification, et quels principes fondamentaux sont plus importants que jamais.