La iziwork, am avut o sarcină de lungă durată în stocul nostru: migrați baza noastră de cod Flow la TypeScript. Suntem bucuroși să raportăm că, după luni de pregătire, am migrat cu succes nucleul nostru de 300k LoC la TypeScript fără a îngheța baza de cod!

În această postare pe blog, vom vedea ce am făcut pentru a face posibilă această migrare. Aceasta este prima parte a unei serii din două părți.

Părăsind Flow

Când a început iziwork, am vrut să beneficiem de instrumente de verificare a tipului. Am ales Flow. Înainte rapid, 3 ani mai târziu, cred că este sigur să spun că Flow și-a pierdut impulsul. Chiar și Facebook, care se află în spatele Flow, a făcut trecerea la TypeScript. TypeScript este acum utilizat pe scară largă în industrie, ecosistemul de tipuri este mult mai bogat, iar instrumentele din jurul TypeScript au evoluat.

Mai mult, majoritatea proiectelor noastre de la iziwork foloseau deja TypeScript. Era timpul ca nucleul nostru să fie migrat la TypeScript.

Obiective

O migrare este o sarcină grea și am avut obiective clare definite:

  • Migrați cât mai repede posibil, fără a compromite calitatea
  • Asigurați-vă că nu vor exista modificări potențiale ale codului, lucrând cât mai mult posibil la nivel de tip. Prin urmare, codul real, compilat, de rulare ar rămâne același
  • iziwork are un ritm rapid, nu am vrut să înghețăm baza de cod. Migrarea ar trebui să se facă în paralel
  • Automatizați ceea ce este automatizat, evitând munca manuală pe cât posibil (de exemplu, jscodeshift)

Acestea erau obiectivele care se potriveau situației noastre de atunci. Restul acestei postări de blog explică lucrurile pe care le-am făcut pentru a rămâne în conformitate cu aceste obiective.

Migrare incrementală VS migrare big bang

Există două căi de migrare posibile:

1. Migrați progresiv de la Flow la TypeScript, având cele două tipuri de fișiere în cod. Acest lucru poate funcționa prin valorificarea puterii overridelor Babel 7

  • Este ușor de revizuit
  • Procesul de migrare se poate face cu ușurință în paralel și durează săptămâni/luni fără a fi nevoie să înghețe codul
  • Reutilizarea tipurilor este dificilă. Nu putem importa un tip Flow în TS și invers
  • Experiența dezvoltatorului IDE este teribilă. Două instrumente de rulat pentru a verifica codul, mult prea multe informații în meniurile contextuale etc.

2. Migrați tot codul într-o singură trecere

  • Revizuirea poate fi destul de complicată
  • Este dificil să faci fără înghețarea bazei de cod (alertă spoiler - este posibil)
  • Experiența dezvoltatorului este destul de bună, IDE-ul trebuie să se ocupe doar de TypeScript
  • Putem scăpa complet de instrumentele Flow

Pe baza feedback-ului pe care îl putem găsi pe Internet, toată lumea tinde să fie de acord că a doua cale este de preferat. Faptul că revizuirea este complicată poate fi atenuat prin existența unui proces clar de urmat și prin automatizarea majorității procesului. Prin urmare, am ales a doua cale: migrați 100% din baza de cod, într-o singură trecere.

Asigurarea unui stil de cod consistent

Pentru a asigura o migrare fără probleme, dorim, pe măsură ce migrăm codul, să avem diferențe curate. Acest lucru va ușura revizuirea diferitelor comisii.

De asemenea, vom rula instrumente de migrare automată, scripturi și vom face o mulțime de căutare și înlocuire în bloc. Adesea, acestea ne vor încurca stilul codului și, în cele din urmă, vor polua istoria noastră Git.

Din acest motiv, este important să utilizați un instrument care vă formatează codul într-un mod determinist. Am ales Prettier, care este aproape instrumentul principal de format de cod în ecosistemul JS.

Puteți alege instrumentul dorit, dar asigurați-vă că codul dvs. Flow este „conform cu stilul” înainte de a începe migrarea.

Implementarea unei plase de siguranță

Prin migrare, vom trece prin multe modificări de cod.

Din acest motiv, avem nevoie de o plasă de siguranță. O modalitate prin care putem să verificăm că, chiar dacă schimbăm mult cod, nu spargem nimic. Într-o lume perfectă, acest lucru este asigurat de suita de teste. Avem o suită de teste, dar există cu siguranță căi de cod care nu sunt testate.

După cum se menționează în obiectivele noastre, vom lucra cât mai mult posibil la nivel de tip. Prin urmare, în mod ideal, codul JS compilat, scos din toate adnotările TypeScript, ar trebui să rămână același cu cel Flow. Având în vedere acest lucru, am avut ideea să facem instantaneu codul JS compilat al bazei noastre de coduri Flow. În acest fel, pe măsură ce migrăm codul la TypeScript, putem instantanee codul compilat, îl putem compara cu punctul nostru de referință (instantaneul Flow) și ne putem asigura că nu spargem nimic.

Pentru a a face asta, trebuie să folosim același instrument de compilare.

În caz contrar, codul emis va fi puțin diferit, iar acest lucru va fi dificil de revizuit pe mii de fișiere. Sunt șanse să utilizați Babel cu pluginul @babel/plugin-transform-flow-strip-types. Din fericire pentru noi, Babel acceptă atât Flow, cât și TypeScript. Aceasta înseamnă că vom putea folosi Babel ca instrument de compilare TypeScript, și nu compilatorul TypeScript. Vom folosi tsc cu opțiunea sa --noEmit, pentru a face doar verificarea tipului.

⚠️ Dacă nu utilizați Babel pentru a transpila codul dvs. Flow (de exemplu, poate folosiți flow-remove-types), ar trebui și puteți migra cu ușurință la Babel.

Jest are o caracteristică grozavă care ne permite să facem exact ceea ce ne dorim: să facem instantaneu codul și să-l comparăm cu instantaneul de referință. Iată testul Jest pe care l-am creat:

Testele de mai sus verifică două lucruri:

  1. Structura fișierului codului compilat ar trebui să rămână aceeași, prin stocarea rezultatului funcției glob
  2. Conținutul fiecărui fișier ar trebui să rămână același

Înarmat cu acest test, acum veți dori să vă construiți codul și să rulați testul, pentru a capta structura fișierului și conținutul fiecărui fișier. Aceasta va genera un fișier .snap mare, care va fi instantaneul de referință folosit de Jest.

💡 Sfat: setați comments: false în .babelrc înainte de a vă captura codul.

Pluginul Flow Babel poate formata comentariile din codul dvs. diferit de pluginul TypeScript. Dezactivarea comentariilor vă va ajuta să revizuiți modificările reale ale codului, în loc de formate de comentarii diferite de prisos.

⚠️ Acum că instantaneul nostru s-a terminat, nu vă rebazați ramura de migrare pe ramura dvs. develop. Acest lucru ar rupe instantaneul. Veți putea prelua modificările din develop mai târziu, când ați migrat întregul cod în ramura de migrare.

Acum, de fiecare dată când veți schimba codul ca parte a migrării, veți parcurge acest proces:

  1. Construiți codul folosind Babel
  2. Rulați testul plasei de siguranță Jest

Dacă trece, bine, codul de rulare nu a fost modificat.
Dacă nu trece, veți putea examina diferența. Dacă sunteți de acord cu modificările, puteți rula jest --updateSnapshot pentru a actualiza instantaneele

După cum sa menționat anterior, de cele mai multe ori, veți lucra la nivel de tip. Dar există momente în care veți avea sau doriți să schimbați timpul de execuție. De exemplu, datorită strictității TypeScript, probabil că veți observa câteva erori pe care Flow nu le-a prins.

Se execută conversia automată a fluxului în TS

Lucrarea inițială de conversie a codului în TypeScript și maparea câtorva tipuri de flux încorporate cu tipuri de TypeScript încorporate (de exemplu, $Keys<T> la keyof T), se poate face automat. Există mai multe instrumente care pot face acest lucru, în special:

  • „Khan/flow-to-ts”
  • „zxbodya/flowts”

Primul este mai „cunoscut”, dar s-a prăbușit fără niciun jurnal atunci când a rulat pe codul nostru.

Acesta din urmă este mai puțin cunoscut, dar oferă mai multă siguranță:

  • Are mai multe mapări de tip automat
  • Se rulează automat Prettier pe codul TypeScript rezultat
  • Acesta verifică dacă JS AST rezultat este exact același înainte și după, afișând avertismente clare și detaliate dacă nu este cazul

Prin urmare, am decis să mergem cu flowts. Puteți încerca ambele instrumente, kilometrajul dvs. poate varia.

Acum puteți rula npx flowts .. Codul dvs. va fi acum convertit în TypeScript. Este posibil să vedeți câteva avertismente și, eventual, o eroare: verificarea eșuată, diferență după eliminarea adnotărilor de tip. Doar verificați diferența și rezolvați problemele dacă există. În baza noastră de cod de 300k, am avut o singură problemă: un șir literal de șablon cu mai multe linii avea 2 spații de prisos. A fost banal de reparat!

Codul nostru este acum în TypeScript. Instrumentul, verificând AST, garantează că codul de ieșire este același, iar diferența după eliminarea adnotărilor de tip este aceeași, ceea ce înseamnă că .js compilat este exact același. Cu alte cuvinte, codul nostru poate fi rulat ca înainte.

Configurarea instrumentelor pentru a suporta TypeScript

Codul nostru este acum scris în TypeScript! Este timpul să ne configuram instrumentele care să o susțină.

Modificarea .babelrc pentru a accepta TypeScript este destul de simplă și este doar o chestiune de adăugare a presetării @babel/preset-typescript. De asemenea, trebuie să actualizăm apelurile Babel CLI pentru a accepta extensia .ts.

💡 Sfat: Setați onlyRemoveTypeImports: true în opțiunile presetării @babel/preset-typescript.

Pluginul Flow Babel elimină doar import type importuri din codul emis. Pluginul TypeScript este puțin mai inteligent: elimină, de asemenea, toate importurile care sunt folosite doar ca tip, chiar dacă nu sunt importate în mod explicit cu sintaxa import type. Pentru a ușura examinarea instantaneelor, dezactivarea acestui comportament cu opțiunea de mai sus vă va asigura că veți păstra aceleași importuri ca înainte. Desigur, puteți (și ar trebui) să reveniți la comportamentul implicit după migrare.

De asemenea, trebuie să configurați biblioteca de testare și linter pentru a accepta TypeScript. Dacă utilizați ESLint, urmați instrucțiunile README-ului typescript-eslint: https://github.com/typescript-eslint/typescript-eslint

Unul dintre cei mai importanți biți de configurare este fișierul tsconfig.json. Deoarece folosim Babel ca instrument de compilare, veți dori să configurați TypeScript pentru a face doar verificarea tipului. Iată, de exemplu, configurația pe care o folosim pentru backend-ul nostru la iziwork:

Observați că nu am activat niciun semnal strict. Indicatoarele stricte pot dubla cu ușurință timpul de migrare necesar pentru a trece tsc. Vă sugerăm să activați semnalizatoarele stricte după migrare, în mod progresiv.

Cu toate instrumentele configurate, nu uitați să vă actualizați scripturile (package.json, conducte CI) pentru a rula tsc în loc de flow check. De asemenea, nu uitați să vă configurați IDE-ul pentru TypeScript.

De asemenea, vă puteți construi codul cu Babel și puteți rula testul rețelei de siguranță Jest. Veți observa că, așa cum este garantat de flowts, codul transpilat al versiunii noastre TypeScript este exact același cu codul transpilat al codului Flow inițial.

Până acum, testele unitare și CI ar trebui să treacă, cu excepția verificării tsc. Succes! Baza de cod și instrumentele dvs. sunt acum activate pentru TypeScript! 🎉

Mai este încă un drum lung de parcurs, totuși...

Fugând tsc fără a fugi

Când veți rula tsc, probabil că veți avea o mulțime de erori. Știm cum se simte: am avut 20 000 de erori.

TypeScript este mai inteligent și mai strict decât Flow în multe aspecte. Dar nu vă descurajați, multe din acest flux inițial de erori sunt foarte ușor de remediat:

  • Probabil că aveți o mulțime de pachete @types/ lipsă
  • Poate că aveți niște aliasuri în codul dvs. care nu sunt prezente în tsconfig.json
  • Unele sintaxe sunt permise în Flow, dar nu și în TypeScript. De exemplu, TypeScript are o eroare TS1095: A 'set' accessor cannot have a return type annotation, în timp ce aceasta este permisă în Flow (cu un tip de returnare de void). Aceste tipuri de erori sunt banale de remediat și pot fi remediate cu codemods, mai multe despre asta mai târziu

Deși multe dintre aceste erori sunt triviale de remediat, este greu de văzut care dintre ele exact când te uiți la ieșirea criptică tsc. Să reparăm asta!

Obținerea unor rapoarte de eroare mai cuprinzătoare de la tsc

Pentru a obține o imagine de ansamblu mai bună asupra erorilor raportate de TypeScript, este important să puteți sintetiza raportul. TypeScript nu oferă o astfel de opțiune, dar puteți scrie cu ușurință un script pentru a gestiona acest lucru. Iată cum arată raportul nostru personalizat (captură de ecran făcută la sfârșitul migrării noastre - am avut mult mai multe erori la început):

Acest raport prezintă două date importante:

  1. Tipurile de erori TS agregate. Acest lucru vă va permite să aruncați o privire asupra situației și veți putea să aruncați o privire la documentația TS și să determinați dacă unele erori sunt greu de remediat
  2. Numărul de erori per fișier, care vă va permite să vedeți care fișiere sunt cele mai problematice. Acest lucru vă va ajuta să prioritizați munca de migrare și să împărțiți sarcinile între mai mulți dezvoltatori

Scriptul rapid și murdar pe care l-am scris pentru a genera raportul de mai sus este „disponibil ca Gist, aici”.

Înarmat cu asta, puteți începe să analizați toate erorile. Probabil veți vedea erori care pot fi remediate automat...

Aplicarea codemod-urilor

Când ne-am analizat propriile erori, am găsit multe dintre acestea:

const foo = {}
foo.bar = true // TS2339: Property 'bar' does not exist on type '{}'

Codul de mai sus este perfect valid în Flow, dar nu este în TypeScript din cauza verificării în exces a proprietăților. Acum, dacă am avea doar câteva dintre aceste erori, le-am putea remedia manual, dar aveam câteva mii de ele. Pentru a ne alinia cu comportamentul Flow, ar trebui să declarăm foo ca any. Trebuie să existe o modalitate de a automatiza asta, nu?

Există într-adevăr mai multe instrumente capabile să realizeze acest lucru. Cel pe care l-am ales este:

  • „facebook/jscodeshift”

jscodeshift este un set de instrumente care ne permite să construim codemod-uri. Un codemod este doar un script care ia ca intrare un AST pe care îl puteți edita. Apoi, jscodeshift scrie codul rezultat înapoi în fișierul inițial. Este mult mai puternic decât regex find/replace.

Iată codul care rezolvă problema de mai sus:

Îl poți rula cu npx jscodeshift --parser ts --extensions ts -t ./codemod.js src.

Pentru a explica pe scurt:

  • Găsește toți declaratorii de variabile în AST
  • Pentru fiecare dintre ele, dacă nu are deja o adnotare de tip, dacă valoarea este o expresie de obiect și dacă acel obiect nu are nicio proprietate, marchem adnotarea de tip ca any

Vă sugerăm să folosiți https://astexplorer.net, vă ajută cu adevărat să scrieți coduri.

Scrieți codemod-uri numai dacă observați erori care sunt remediate sistematic în același mod și care ar fi prea lungi pentru a fi remediate manual. Nu are sens să scrieți un codemod dacă scrierea durează mai mult decât doar rezolvarea manuală a problemei.

Sperăm că veți avea câteva sute sau mii de erori rămase. Când credeți că ați terminat cu codemod-urile, este timpul să remediați manual erorile rămase!

Remedierea erorilor rămase manual

Să recunoaștem, acesta este cel mai plictisitor pas al migrației, dar cel mai plin de satisfacții, deoarece până la sfârșit vei vedea tsc trecând.

Până acum, ar trebui să aveți o idee precisă despre care sunt erorile rămase. Privind raportul dvs. personalizat tsc, ar trebui să vă pregătiți mai întâi o listă de erori pe care să le rezolvați. A avea 2000 de erori rămase nu înseamnă că va trebui să remediați erori în 2000 de locuri; prin remedierea unei erori undeva, ați putea remedia 100 de erori în altă parte. De aceea este important să aveți un plan de acțiune clar.

De acum înainte, are sens să nu fii singurul care lucrează la migrare, deoarece poți începe să migrați lucrurile simultan.

La un moment dat, veți descoperi că toate erorile rămase nu au legătură și nu mai pot fi prioritizate cu adevărat. Dacă sunteți doi dezvoltatori care lucrează la migrare, unul dintre dezvoltatori ar putea remedia erori începând din partea de sus a listei de fișiere, în timp ce celălalt ar putea să repare erorile din partea de jos. În acest fel, veți lucra eficient, asigurându-vă în același timp că nu vă veți suprapune cu colegii dvs.

Pentru referință, la iziwork, acest pas ne-a luat 2 zile pentru a finaliza pentru 2 dezvoltatori, cu aproximativ 2000 de erori de remediat manual.

Toate bune, acum aveți o ramură TypeScript funcțională.

Încă nu ai terminat! În partea următoare, vom vedea cum să vă actualizați ramura TypeScript cu develop, care probabil a evoluat de la începutul migrării și cum să migrați toate ramurile paralele la care au lucrat colegii dvs. în cele din urmă îmbinând tot codul TypeScript.

Rămâneţi aproape!