Stratégies de migration
Traduire un projet à grande échelle de JavaScript vers TypeScript est un défi. Avant de commencer à le résoudre, nous avons étudié deux stratégies pour passer de JS à TS.
▍1. Stratégie de migration hybride
Avec cette approche, une traduction progressive, fichier par fichier, du projet en TypeScript est effectuée. Au cours de ce processus, ils éditent les fichiers, corrigent les erreurs de frappe et travaillent de cette façon jusqu'à ce que l'ensemble du projet soit traduit en TS. Le paramètre allowJS vous permet d'avoir à la fois des fichiers TypeScript et des fichiers JavaScript dans votre projet. Grâce à cela, cette approche de la traduction de projets JS en TS est tout à fait viable.
Avec une stratégie de migration hybride, vous n'avez pas besoin de suspendre le processus de développement, vous pouvez progressivement, fichier par fichier, traduire le projet en TypeScript. Mais, si nous parlons d'un projet à grande échelle, ce processus peut prendre du temps. Cela nécessite également une formation pour les programmeurs de toute l'organisation. Les programmeurs devront être familiarisés avec les spécificités du projet.
▍2. Stratégie de migration complète
Cette approche prend un projet écrit entièrement en JavaScript, ou dont une partie est écrite en TypeScript, et le transforme complètement en un projet TypeScript. Dans ce cas, vous devrez utiliser le type
any
et les commentaires @ts-ignore
, ce qui permettra au projet de se compiler sans erreur. Mais au fil du temps, le code peut être modifié et passer à l'utilisation de types plus appropriés.
La stratégie globale de migration de TypeScript présente plusieurs avantages significatifs par rapport à la stratégie hybride:
- . , , . , TypeScript, , .
- , . , , , . .
Compte tenu de ce qui précède, il semblerait que la migration omniprésente soit supérieure à la migration hybride à tous égards. Mais traduire une base de code mature en TypeScript de manière globale est une tâche très difficile. Pour le résoudre, nous avons décidé de recourir à des scripts pour modifier le code, aux soi-disant "codemods" ( codemods ). Lorsque nous avons commencé à traduire un projet en TypeScript, en le faisant manuellement, nous avons remarqué des opérations répétitives qui pouvaient être automatisées. Nous avons écrit des mods de code pour chacune de ces opérations et les avons combinés en un seul pipeline de migration.
L'expérience nous montre que nous ne pouvons pas être sûrs à 100% qu'après la traduction automatique d'un projet en TypeScript, il n'y aura pas d'erreurs. Mais nous avons découvert que la combinaison des étapes décrites ci-dessous nous a donné les meilleurs résultats et, à la fin, nous avons obtenu un projet TypeScript sans erreurs. À l'aide de mods de code, nous avons pu traduire en TypeScript un projet contenant plus de 50 000 lignes de code et représenté par plus de 1 000 fichiers. Il nous a fallu un jour pour le faire.
Sur la base du pipeline illustré dans la figure suivante, nous avons créé l'outil ts-migrate.
Modemods Ts-migrate
Dans Airbnb, une grande partie du front-end est écrite à l'aide de React . C'est pourquoi certaines parties du code mod sont liées à des concepts spécifiques à React. L'outil ts-migrate peut être utilisé avec d'autres bibliothèques ou frameworks, mais cela nécessitera une configuration et des tests supplémentaires.
Aperçu du processus de migration
Passons en revue les étapes de base que vous devez suivre pour traduire un projet de JavaScript vers TypeScript. Parlons de la manière dont ces étapes sont mises en œuvre.
▍Étape 1
La première chose que crée chaque projet TypeScript est un fichier
tsconfig.json
. Ts-migrate peut le faire seul si nécessaire. Il existe un modèle standard pour ce fichier. De plus, un système de vérification est en place pour garantir que tous les projets sont configurés de manière cohérente. Voici un exemple de configuration de base:
{
"extends": "../typescript/tsconfig.base.json",
"include": [".", "../typescript/types"]
}
▍Étape 2
Une fois que le fichier
tsconfig.json
est là où il devrait être, les fichiers source sont renommés. À savoir, les extensions .js / .jsx deviennent .ts / .tsx. Cette étape est très facile à automatiser. Cela vous permet de vous débarrasser d'une grande quantité de travail manuel.
▍Étape 3
Il est maintenant temps d'exécuter les mods de code! Nous les appelons des plugins. Les plugins pour ts-migrate sont des mods de code qui ont accès à des informations supplémentaires via le serveur de langage TypeScript. Les plugins acceptent les chaînes comme entrée et renvoient des chaînes modifiées. La boîte à outils jscodeshift , l'API TypeScript, les outils de traitement de chaînes ou d'autres outils de modification AST peuvent être utilisés pour effectuer des transformations de code .
Après avoir terminé chacune des étapes ci-dessus, nous vérifions s'il y a des modifications en attente dans l'historique Git et les incluons dans le projet. Cela vous permet de diviser les PR de migration en commits, ce qui facilite la compréhension de ce qui se passe et permet de suivre les changements dans les noms de fichiers.
Présentation des packages qui composent ts-migrate
Nous avons divisé ts-migrate en 3 packages:
En faisant cela, nous avons pu séparer la logique de transformation de code du cœur du système et avons pu créer de nombreuses configurations conçues pour résoudre différents problèmes. Nous avons maintenant deux configurations principales: migration et reignore .
Le but de l'application de la configuration
migration
est de traduire le projet de JavaScript vers TypeScript. Et la configuration reignore
est utilisée pour permettre de compiler le projet en ignorant simplement les erreurs. Cette configuration est utile lorsque vous avez une base de code volumineuse et que vous en faites diverses choses, comme ce qui suit:
- Mise à jour de la version TypeScript.
- Apporter des modifications majeures au code ou refactoriser la base de code.
- Amélioration des types de certaines bibliothèques couramment utilisées.
Avec cette approche, nous pouvons traduire le projet en TypeScript même si, lors de la compilation, des erreurs sont générées que nous ne prévoyons pas de traiter immédiatement. Cela facilite également la mise à jour de TypeScript ou des bibliothèques utilisées dans votre code.
Les deux configurations s'exécutent sur un serveur
ts-migrate-server
composé de deux parties:
- TSServer : Cette partie du serveur est très similaire à ce que VSCode utilise pour communiquer entre l'éditeur et le serveur de langue. La nouvelle instance du serveur de langage TypeScript démarre dans un processus distinct. Les outils de développement interagissent avec lui à l'aide d'un protocole de langage .
- Outil de migration : il s'agit du code qui effectue le processus de migration et coordonne ce processus. Cet outil accepte les paramètres suivants:
interface MigrateParams {
rootDir: string; // .
config: MigrateConfig; // ,
// .
server: TSServer; // TSServer.
}
Cet outil effectue les opérations suivantes:
- Analyse du fichier
tsconfig.json
. - Création de fichiers .ts avec le code source.
- Envoyez chaque fichier au serveur de langage TypeScript pour diagnostiquer ce fichier. Il existe trois types de diagnostics, ce qui nous donne le compilateur:
semanticDiagnostics
,syntacticDiagnostics
etsuggestionDiagnostics
. Nous utilisons ces vérifications pour trouver les zones problématiques dans le code source. Sur la base du code de diagnostic unique et du numéro de ligne du fichier, nous pouvons identifier le type de problème possible et appliquer les modifications de code nécessaires. - Traitement de chaque fichier par tous les plugins. Si le texte du fichier a changé à l'initiative du plugin, nous mettons à jour le contenu du fichier d'origine et notifions au serveur de langue que le fichier a été modifié.
Des exemples d'utilisation
ts-migrate-server
peuvent être trouvés dans le package des exemples ou dans le package principal . Il ts-migrate-example
contient également des exemples de plugins de base . Ils se répartissent en 3 catégories principales:
- Plugins basés sur jscodeshift.
- Plugins basés sur le TypeScript Abstract Syntax Tree (AST).
- Plugins de traitement de texte.
Le référentiel contient un ensemble d'exemples visant à démontrer le processus de création de plugins simples de toutes ces sortes. Il montre également leur utilisation en combinaison c
ts-migrate-server
. Voici un exemple de pipeline de migration qui transforme le code. Le code suivant est reçu à son entrée:
function mult(first, second) {
return first * second;
}
Et il donne ce qui suit:
function tlum(tsrif: number, dnoces: number): number {
console.log(`args: ${arguments}`);
return tsrif * dnoces;
}
Dans cet exemple, ts-migrate a effectué 3 transformations:
- Il inverse l'ordre des caractères dans tous les identifiants:
first -> tsrif
. - Ajout d' informations sur les types dans la déclaration de fonction:
function tlum(tsrif, dnoces) -> function tlum(tsrif: number, dnoces: number): number
. - Ajout de la ligne au code
console.log(‘args:${arguments}’);
Plugins à usage général
Les vrais plugins sont situés dans un package séparé - ts-migrate-plugins . Jetons un coup d'œil à certains d'entre eux. Nous avons deux plugins basés sur jscodeshift:
explicitAnyPlugin
et declareMissingClassPropertiesPlugin
. La boîte à outils jscodeshift vous permet de convertir les AST en code normal à l'aide du package de refonte . Nous pouvons utiliser la fonction toSource()
pour mettre à jour directement le code source contenu dans nos fichiers.
Le plugin explicitAnyPlugin récupère des informations du serveur de langage TypeScript sur toutes les erreurs
semanticDiagnostics
et les lignes dans lesquelles ces erreurs ont été détectées. Ensuite, l'annotation de type est ajoutée à ces lignes any
. Cette approche vous permet de corriger les erreurs, car l'utilisation du typeany
vous permet de vous débarrasser des erreurs de compilation.
Voici un exemple de code avant le traitement:
const fn2 = function(p3, p4) {}
const var1 = [];
Voici le même code traité par le plugin:
const fn2 = function(p3: any, p4: any) {}
const var1: any = [];
Le declareMissingClassPropertiesPlugin prend tous les messages de diagnostic avec un code d'erreur
2339
(pouvez-vous deviner ce que ce code signifie ?) Et, s'il peut trouver des déclarations de classe avec des identificateurs manquants, les ajoute au corps de la classe annotée any
. À partir du nom du plugin, nous pouvons conclure qu'il n'est applicable qu'aux classes ES6 .
La catégorie suivante de plugins est basée sur AST TypeScript. En traitant l'AST, nous pouvons générer un tableau de mises à jour à apporter au fichier source. Les descriptions de ces mises à jour ressemblent à ceci:
type Insert = { kind: 'insert'; index: number; text: string };
type Replace = { kind: 'replace'; index: number; length: number; text: string };
type Delete = { kind: 'delete'; index: number; length: number };
Après avoir généré des informations sur les mises à jour nécessaires, il ne reste plus qu'à les saisir dans le fichier dans l'ordre inverse. Si, après avoir effectué cette opération, nous recevons un nouveau code de programme, nous mettrons à jour le fichier de code source en conséquence.
Jetons un coup d'œil aux prochains plugins basés sur AST. C'est
stripTSIgnorePlugin
et hoistClassStaticsPlugin
.
Le plugin stripTSIgnorePlugin est le premier plugin utilisé dans le pipeline de migration. Il supprime tous les commentaires du fichier.
@ts-ignore
(ces commentaires nous permettent de dire au compilateur d'ignorer les erreurs qui se produisent sur la ligne suivante). Si nous traduisons un projet écrit en JavaScript en TypeScript, ce plugin n'effectuera aucune action. Mais si nous parlons d'un projet en partie écrit en JS, et en partie en TS (plusieurs de nos projets étaient dans un état similaire), alors c'est la première étape de migration dont on ne peut pas se passer. Ce n'est qu'après la suppression des commentaires que @ts-ignore
le compilateur TypeScript génère des messages d'erreur de diagnostic qui doivent être corrigés.
Voici le code qui entre dans l'entrée de ce plugin:
const str3 = foo
? // @ts-ignore
// @ts-ignore comment
bar
: baz;
Voici le résultat:
const str3 = foo
? bar
: baz;
Après avoir supprimé les commentaires,
@ts-ignore
nous exécutons le plugin hoistClassStaticsPlugin . Il passe par toutes les déclarations de classe. Le plugin détermine la possibilité d'élever des identifiants ou des expressions et découvre si une opération d'affectation a déjà été élevée au niveau de la classe.
Afin d'assurer une vitesse de développement élevée et d'éviter les rétrogradations forcées vers les versions précédentes du projet, nous avons fourni à chaque plugin et ts-migrate un ensemble de tests unitaires.
Plugins liés à React
Le reactPropsPlugin , qui s'appuie sur cet outil génial , convertit les informations de type de PropTypes en déclarations de type TypeScript. Avec ce plugin, vous ne devez traiter que les fichiers .tsx contenant au moins un composant React. Ce plugin recherche toutes les déclarations PropTypes et essaie de les analyser en utilisant des AST et des expressions régulières simples comme
/number/
, ou en utilisant des expressions régulières plus complexes comme / objectOf $ / . Lorsqu'il est détecté React-composant (fonction ou à base de classe A ), il est transformé en un composant dans lequel un nouveau type est utilisé pour les paramètres d'entrée ( des accessoires): type Props = {…};
.
Plugin ReactDefaultPropsPluginest responsable de l'implémentation du modèle defaultProps dans les composants React . Nous utilisons un type spécial pour représenter les paramètres d'entrée qui reçoivent des valeurs par défaut:
type Defined<T> = T extends undefined ? never : T;
type WithDefaultProps<P, DP extends Partial<P>> = Omit<P, keyof DP> & {
[K in Extract<keyof DP, keyof P>]:
DP[K] extends Defined<P[K]>
? Defined<P[K]>
: Defined<P[K]> | DP[K];
};
Nous essayons de trouver les accessoires auxquels des valeurs par défaut ont été attribuées, puis nous les combinons avec le type qui décrit les accessoires pour le composant que nous avons créé à l'étape précédente.
L'écosystème React utilise largement les concepts d' état et de cycle de vie des composants. Nous abordons les problèmes liés à ces concepts dans les prochains plugins. Ainsi, si le composant a un état, alors le plugin reactClassStatePlugin génère un nouveau type (
type State = any;
), et le plugin reactClassLifecycleMethodsPlugin annote les méthodes de cycle de vie des composants avec les types correspondants. La fonctionnalité de ces plugins peut être étendue, notamment en leur donnant la possibilité de les remplacer par any
des types plus précis.
Ces plugins peuvent être améliorés, en particulier, en étendant la prise en charge des types pour l'état et les propriétés. Mais leurs capacités existantes, il s'est avéré, sont un bon point de départ pour mettre en œuvre les fonctionnalités dont nous avons besoin. De plus, nous ne travaillons pas avec les hooks React ici , car au début de la migration, notre base de code utilisait une ancienne version de React qui ne supportait pas les hooks.
Vérifier que le projet est correctement compilé
Notre objectif est de compiler un projet TypeScript équipé de types de base sans changer le comportement du programme.
Après toutes les transformations et modifications, notre code peut s'avérer être formaté de manière non uniforme, ce qui peut conduire au fait que certaines vérifications de code avec le linter révèlent des erreurs. Notre base de code frontend utilise un système basé sur Prettier et ESLint. À savoir, Prettier est utilisé pour le formatage automatique du code et ESLint aide à vérifier la conformité du code avec les approches de développement recommandées. Tout cela nous permet de traiter rapidement les problèmes de formatage de code résultant d'actions précédentes, simplement en utilisant le plugin approprié .-
eslintFixPlugin
.
La dernière étape du pipeline de migration consiste à vérifier que tous les problèmes de compilation TypeScript ont été résolus. Afin de trouver et de corriger les erreurs potentielles, le plugin tsIgnorePlugin prend les informations du diagnostic sémantique du code et des numéros de ligne, puis ajoute des commentaires au code
@ts-ignore
avec des explications sur les erreurs. Par exemple, cela pourrait ressembler à ceci:
// @ts-ignore ts-migrate(7053) FIXME: No index signature with a parameter of type 'string...
const { field1, field2, field3 } = DATA[prop];
// @ts-ignore ts-migrate(2532) FIXME: Object is possibly 'undefined'.
const field2 = object.some_property;
Nous avons équipé le système du support de la syntaxe JSX:
{*
// @ts-ignore ts-migrate(2339) FIXME: Property 'NORMAL' does not exist on type 'typeof W... */}
<Text weight={WEIGHT.NORMAL}>
some text
</Text>
<input
id="input"
// @ts-ignore ts-migrate(2322) FIXME: Type 'Element' is not assignable to type 'string'.
name={getName()}
/>
La mise à disposition de messages d'erreur significatifs facilite la correction des erreurs et la recherche d'extraits de code à rechercher. Les commentaires pertinents, en combinaison avec
$TSFixMe
, nous permettent de collecter des données précieuses sur la qualité du code et de trouver des fragments de code potentiellement problématiques. $TSFixMe
Est l'alias de type que nous avons créé any
. Et pour les fonctions, c'est $TSFixMeFunction = (…args: any[]) => any;
. Il est recommandé d'éviter d'utiliser un type any
, mais son utilisation nous a permis de simplifier le processus de migration. L'utilisation de ce type nous a aidés à savoir exactement quels fragments de code devaient être améliorés.
Il est à noter que le plugin
eslintFixPlugin
s'exécute deux fois. Première fois avant utilisationtsIgnorePlugin
car le formatage peut affecter les messages sur l'emplacement des erreurs de compilation. La deuxième fois, c'est après l'application tsIgnorePlugin
, car l'ajout de commentaires au code @ts-ignore
peut entraîner des erreurs de formatage.
Notes complémentaires
Nous aimerions attirer votre attention sur quelques fonctionnalités de migration que nous avons remarquées pendant le travail. Peut-être que la connaissance de ces fonctionnalités vous sera utile lorsque vous travaillez avec vos projets.
- TypeScript 3.7 @ts-nocheck, TypeScript- . , .js-, .ts/.tsx-. , .
- TypeScript 3.9 introduit la prise en charge des commentaires @ ts-expect-error . Si une ligne de code est précédée d'un tel commentaire, TypeScript ne signalera pas l'erreur correspondante. S'il n'y a pas d'erreur dans une telle ligne, TypeScript
@ts-expect-error
vous informera qu'il n'y a pas besoin de commentaire . La base de code Airbnb est passée des commentaires@ts-ignore
aux commentaires@ts-expect-error
.
Résultat
La migration de la base de code d'Airbnb de JavaScript vers TypeScript est toujours en cours. Nous avons quelques anciens projets qui sont toujours représentés par du code JavaScript.
$TSFixMe
Les commentaires sont toujours courants dans notre base de code @ts-ignore
.
JavaScript et TypeScript dans Airbnb
Mais il faut noter que l'utilisation de ts-migrate a grandement accéléré le processus de traduction de nos projets de JS vers TS et a grandement amélioré la productivité de notre travail. Avec ts-migrate, les programmeurs ont pu se concentrer sur l'amélioration de la saisie plutôt que sur le traitement manuel de chaque fichier. Actuellement, environ 86% de notre mono-référentiel frontal, qui compte environ 6 millions de lignes de code, est traduit en TypeScript. Nous prévoyons d'atteindre 95% d'ici la fin de cette année.
Ici, sur la page d'accueil du référentiel de projet, vous pouvez apprendre comment installer et exécuter ts-migrate. Si vous rencontrez des problèmes dans ts-migrate, ou si vous avez des idées pour améliorer cet outil, nous vous invitons à nous rejoindre.pour y travailler!
Avez-vous déjà traduit de gros projets de JavaScript vers TypeScript?