Transfer 30.000 baris kode dari Flow ke TypeScript

Kami baru-baru ini memindahkan 30.000 baris kode JavaScript dari sistem MemSQL Studio kami dari Flow ke TypeScript. Pada artikel ini saya akan menjelaskan mengapa kami mem-porting basis kode, bagaimana itu terjadi dan apa yang terjadi.

Penafian: Tujuan saya bukan untuk mengkritik Flow sama sekali. Saya mengagumi proyek dan berpikir ada cukup ruang di komunitas JavaScript untuk kedua jenis opsi pemeriksaan. Pada akhirnya, semua orang akan memilih yang paling cocok untuknya. Saya sangat berharap artikel ini akan membantu dalam pilihan ini.

Pertama, saya akan memberi Anda informasi terkini. Kami di MemSQL adalah penggemar berat pengetikan JavaScript statis dan kuat untuk menghindari masalah umum dengan pengetikan dinamis dan lemah.

Pidato tentang masalah umum:

  1. Ketik kesalahan dalam runtime karena fakta bahwa berbagai bagian kode tidak cocok dengan tipe implisit.
  2. Terlalu banyak waktu dihabiskan untuk menulis tes untuk hal-hal sepele seperti memeriksa parameter tipe (memeriksa runtime juga meningkatkan ukuran paket).
  3. Ada kekurangan integrasi editor / IDE, karena tanpa pengetikan statis akan jauh lebih sulit untuk mengimplementasikan fungsi Langsung ke Definisi, refactoring mekanis, dan fungsi lainnya.
  4. Tidak ada cara untuk menulis kode di sekitar model data, yaitu, tipe data desain pertama, dan kemudian kode pada dasarnya "menulis sendiri".

Ini hanya beberapa manfaat dari pengetikan statis, yang selanjutnya tercantum dalam artikel terbaru tentang Flow .

Pada awal 2016, kami menerapkan tcomb untuk mengimplementasikan beberapa jenis keamanan dalam runtime dari salah satu proyek JavaScript internal kami (penafian: Saya tidak terlibat dalam proyek ini). Meskipun pengecekan runtime terkadang berguna, itu bahkan tidak memberikan semua manfaat pengetikan statis (kombinasi pengetikan statis dan pengecekan tipe dalam runtime mungkin cocok untuk kasus-kasus tertentu, io-ts memungkinkan Anda melakukan ini dengan tcomb dan TypeScript, meskipun saya belum pernah mencoba ) Memahami hal ini, kami memutuskan untuk mengimplementasikan Flow untuk proyek lain yang kami mulai pada tahun 2016. Pada saat itu, Flow sepertinya pilihan yang bagus:

  • Dukungan dari Facebook, yang telah melakukan pekerjaan luar biasa dalam mengembangkan React dan menumbuhkan komunitas (mereka juga mengembangkan React with Flow).
  • Kira-kira ekosistem pengembangan JavaScript yang sama. Sangat menakutkan untuk meninggalkan Babel untuk tsc (compiler TypeScript) karena kami telah kehilangan fleksibilitas untuk beralih ke pemeriksaan tipe lain (jelas, situasinya telah berubah sejak saat itu).
  • Tidak perlu mengetikkan seluruh basis kode (kami ingin mendapatkan gagasan tentang JavaScript yang diketik secara statis sebelum masuk semua), tetapi hanya sebagian dari file. Harap dicatat bahwa baik Flow dan TypeScript sekarang mengizinkan ini.
  • TypeScript (pada waktu itu) tidak memiliki beberapa fungsi dasar yang sekarang tersedia, ini adalah tipe pencarian , parameter default untuk tipe generik , dll.

Ketika kami mulai bekerja pada MemSQL Studio pada akhir 2017, kami akan membahas jenis-jenis seluruh aplikasi (seluruhnya ditulis dalam JavaScript: baik frontend dan backend dieksekusi di browser). Kami mengambil Flow sebagai alat yang kami berhasil gunakan di masa lalu.

Tapi perhatian saya tertarik ke Babel 7 dengan dukungan TypeScript . Rilis ini berarti bahwa beralih ke TypeScript tidak lagi memerlukan transisi ke seluruh ekosistem TypeScript, dan Anda dapat terus menggunakan Babel untuk JavaScript. Lebih penting lagi, kita bisa menggunakan TypeScript hanya untuk memeriksa jenis , dan bukan sebagai "bahasa" lengkap.

Secara pribadi, saya percaya bahwa memisahkan pemeriksaan jenis dari generator kode adalah cara yang lebih elegan untuk mengetik statis dalam JavaScript, karena:

  1. Kami berbagi masalah kode dan pengetikan. Ini mengurangi berhenti pengecekan tipe dan mempercepat pengembangan: jika karena alasan tertentu pengecekan tipe lambat, kode masih akan menghasilkan dengan benar (jika Anda menggunakan tsc dengan Babel, Anda dapat mengonfigurasinya untuk melakukan hal yang sama).
  2. Babel memiliki plugin dan fitur hebat yang tidak dimiliki generator TypeScript. Misalnya, Babel memungkinkan Anda untuk menentukan browser yang didukung dan secara otomatis akan mengeluarkan kode untuk mereka. Ini adalah fungsi yang sangat kompleks dan tidak masuk akal untuk mendukungnya dalam dua proyek berbeda secara bersamaan.
  3. Saya suka JavaScript sebagai bahasa pemrograman (kecuali untuk kurangnya pengetikan statis), dan saya tidak tahu berapa banyak TypeScript akan ada, sementara saya percaya pada ECMAScript selama bertahun-tahun. Oleh karena itu, saya lebih suka menulis dan "berpikir" dalam JavaScript (perhatikan bahwa saya mengatakan "gunakan Flow" atau "gunakan TypeScript" daripada "tulis dalam Flow" atau "TypeScript", karena saya selalu mewakili mereka dengan alat, bukan bahasa pemrograman).

Tentu saja, pendekatan ini memiliki beberapa kelemahan:

  1. Compiler TypeScript secara teoritis dapat melakukan optimasi berbasis tipe, tetapi di sini kita kehilangan kesempatan itu.
  2. Konfigurasi proyek sedikit lebih rumit dengan peningkatan jumlah alat dan dependensi. Saya pikir ini adalah argumen yang relatif lemah: sekelompok Babel dan Flow tidak pernah mengecewakan kita.

TypeScript sebagai alternatif untuk Flow


Saya melihat minat yang tumbuh pada TypeScript di komunitas JavaScript: baik online dan di antara pengembang di sekitarnya. Oleh karena itu, segera setelah saya mengetahui bahwa Babel 7 mendukung TypeScript, saya segera mulai mempelajari opsi transisi yang potensial. Selain itu, kami menemukan beberapa kelemahan dari Flow:

  1. Kualitas integrasi editor / IDE yang lebih rendah (dibandingkan dengan TypeScript). Nuclide, IDE milik Facebook dengan integrasi terbaik, sudah ketinggalan zaman.
  2. Komunitas yang lebih kecil, yang berarti lebih sedikit definisi tipe untuk pustaka yang berbeda, dan mereka memiliki kualitas yang lebih rendah (saat ini repositori DefinitelyTyped memiliki 19.682 bintang GitHub, dan repositori tipe aliran hanya memiliki 3070).
  3. Kurangnya rencana pengembangan publik dan interaksi yang buruk antara tim Flow di Facebook dan komunitas. Anda dapat membaca komentar ini dari karyawan Facebook untuk memahami situasinya.
  4. Konsumsi memori yang tinggi dan kebocoran yang sering terjadi - untuk beberapa pengembang kami, Flow terkadang memakan hampir 10 GB RAM.

Tentu saja, Anda harus mempelajari bagaimana TypeScript cocok untuk kita. Ini adalah pertanyaan yang sangat kompleks: mempelajari topik termasuk membaca dokumentasi secara menyeluruh, yang membantu untuk memahami bahwa untuk setiap fungsi Flow terdapat TypeScript yang setara. Kemudian saya menjelajahi rencana pengembangan publik TypeScript, dan saya sangat menyukai fitur-fitur yang direncanakan untuk masa depan (misalnya, turunan sebagian dari argumen tipe yang kami gunakan dalam Flow).

Transfer lebih dari 30 ribu baris kode dari Flow ke TypeScript


Sebagai permulaan, Anda harus memutakhirkan Babel dari 6 menjadi 7. Tugas sederhana ini memakan waktu 16 orang-jam, karena kami memutuskan untuk memutakhirkan Webpack 3 menjadi 4. Pada saat yang sama, beberapa dependensi usang dalam kode kami mempersulit tugas. Sebagian besar proyek JavaScript tidak akan memiliki masalah seperti itu.

Setelah itu, kami mengganti preset Babel Flow dengan preset TypeScript yang baru, dan kemudian untuk pertama kalinya meluncurkan kompiler TypeScript pada semua sumber kami yang ditulis menggunakan Flow. Hasilnya adalah 8245 kesalahan sintaks (CLI tsc tidak menunjukkan kesalahan nyata untuk proyek sampai semua kesalahan sintaks telah diperbaiki).

Pada awalnya, angka ini membuat kami takut (sangat), tetapi kami segera menyadari bahwa sebagian besar kesalahan adalah karena TypeScript tidak mendukung file .js. Setelah mempelajari topik tersebut, saya belajar bahwa file TypeScript harus diakhiri dengan .ts atau .tsx (jika memiliki JSX). Bagi saya ini sepertinya ketidaknyamanan yang jelas. Agar tidak memikirkan ada / tidaknya JSX, saya cukup mengganti nama semua file menjadi .tsx.

Sekitar 4.000 kesalahan sintaks masih ada. Kebanyakan dari mereka terkait dengan impor tipe , yang dengan TypeScript dapat diganti hanya dengan impor, serta perbedaan dalam penunjukan objek ( {||} bukan {} ). Dengan cepat menerapkan beberapa ekspresi reguler, kami meninggalkan 414 kesalahan sintaks. Segala sesuatu yang lain harus diperbaiki secara manual:

  • Tipe eksistensial , yang kami gunakan untuk memperoleh sebagian argumen dari tipe generik, harus diganti dengan argumen eksplisit atau tidak dikenal untuk memberi tahu TypeScript bahwa beberapa argumen tidak penting.
  • Ketik $ Keys dan tipe Flow canggih lainnya memiliki sintaks yang berbeda dalam TypeScript (misalnya, $Shapeβ€œβ€ sesuai dengan Partialβ€œβ€ di TypeScript).

Setelah memperbaiki semua kesalahan sintaksis, tsc akhirnya mengatakan berapa banyak kesalahan tipe nyata dalam basis kode kami hanya sekitar 1.300. Sekarang kami harus duduk dan memutuskan apakah akan melanjutkan atau tidak. Lagi pula, jika migrasi membutuhkan waktu berminggu-minggu, yang terbaik adalah tetap menggunakan Flow. Namun, kami memutuskan bahwa porting kode akan membutuhkan kurang dari satu minggu kerja oleh satu insinyur, yang cukup dapat diterima.

Harap dicatat bahwa selama migrasi saya harus menghentikan semua pekerjaan pada basis kode ini. Namun demikian, secara paralel Anda dapat memulai proyek baru - tetapi Anda harus mengingat secara potensial ratusan jenis kesalahan dalam kode yang ada, yang tidak mudah.

Kesalahan macam apa?


TypeScript dan Proses aliran kode JavaScript dalam banyak cara. Jadi, Flow lebih ketat dalam kaitannya dengan beberapa hal, dan TypeScript - dalam kaitannya dengan yang lain. Perbandingan yang mendalam dari kedua sistem akan sangat panjang, jadi lihat saja beberapa contoh.

Catatan: semua tautan ke kotak pasir TypeScript menggunakan parameter "ketat". Sayangnya, saat Anda membagikan tautan, opsi ini tidak disimpan di URL. Oleh karena itu, mereka harus diatur secara manual setelah membuka tautan apa pun ke kotak pasir dari artikel ini.

invariant.js


Fungsi invariant ternyata sangat umum dalam kode sumber kami. Hanya mengutip dokumentasi:

 var invariant = require('invariant'); invariant(someTruthyVal, 'This will not throw'); // No errors invariant(someFalseyVal, 'This will throw an error with this message'); // Error raised: Invariant Violation: This will throw an error with this message 

Idenya jelas: fungsi sederhana yang melempar kesalahan pada beberapa kondisi. Mari kita lihat bagaimana menerapkan dan menggunakannya pada Flow:

 type Maybe<T> = T | void; function invariant(condition: boolean, message: string) { if (!condition) { throw new Error(message); } } function f(x: Maybe<number>, c: number) { if (c > 0) { invariant(x !== undefined, "When c is positive, x should never be undefined"); (x + 1); // works because x has been refined to "number" } } 

Sekarang muat cuplikan yang sama di TypeScript . Seperti yang dapat Anda lihat dari tautan, TypeScript memberikan kesalahan, karena itu tidak dapat memahami bahwa x dijamin tidak akan tetap undefined setelah baris terakhir. Ini sebenarnya masalah yang terkenal - TypeScript (untuk saat ini) tidak tahu bagaimana melakukan inferensi ini melalui suatu fungsi. Namun, ini adalah template yang sangat umum di basis kode kami, jadi saya harus mengganti secara manual setiap instance invarian (lebih dari 150 buah) dengan kode lain yang segera memberikan kesalahan:

 type Maybe<T> = T | void; function f(x: Maybe<number>, c: number) { if (c > 0) { if (x === undefined) { throw new Error("When c is positive, x should never be undefined"); } (x + 1); // works because x has been refined to "number" } } 

Tidak benar-benar dibandingkan dengan invariant , tetapi bukan masalah penting.

$ ExpectError vs @ ts-abaikan


Flow memiliki fungsi yang sangat menarik, mirip dengan @ts-ignore , kecuali bahwa ia melempar kesalahan jika baris berikutnya bukan kesalahan. Ini sangat berguna untuk menulis "tes tipe" yang memastikan bahwa pemeriksaan tipe (apakah TypeScript atau Flow) menemukan kesalahan tipe tertentu.

Sayangnya, TypeScript tidak memiliki fungsi seperti itu, jadi pengujian kami telah kehilangan beberapa nilai. Saya berharap dapat mengimplementasikan fungsi ini pada TypeScript .

Kesalahan tipe umum dan inferensi tipe


Seringkali TypeScript memungkinkan kode yang lebih eksplisit daripada Flow, seperti dalam contoh ini:

 type Leaf = { host: string; port: number; type: "LEAF"; }; type Aggregator = { host: string; port: number; type: "AGGREGATOR"; } type MemsqlNode = Leaf | Aggregator; function f(leaves: Array<Leaf>, aggregators: Array<Aggregator>): Array<MemsqlNode> { // The next line errors because you cannot concat aggregators to leaves. return leaves.concat(aggregators); } 

Flow infers leaves.concat (agregator) ketik sebagai Array <Leaf | Aggregator> , yang kemudian dapat Array<MemsqlNode> ke Array<MemsqlNode> . Saya pikir ini adalah contoh yang bagus di mana Flow sedikit lebih pintar, dan TypeScript membutuhkan sedikit bantuan: dalam hal ini kita dapat menerapkan pernyataan tipe, tapi ini berbahaya dan harus dilakukan dengan sangat hati-hati.

Meskipun saya tidak memiliki bukti formal, saya percaya Flow jauh lebih unggul daripada TypeScript dalam inferensi tipe. Saya sangat berharap bahwa TypeScript akan mencapai level Flow, karena bahasa ini sangat aktif berkembang, dan banyak perbaikan baru-baru ini telah dibuat di bidang ini. Di banyak tempat dalam kode kami, TypeScript harus membantu sedikit melalui penjelasan atau mengetik pernyataan, meskipun kami menghindari yang terakhir sebanyak mungkin). Mari kita pertimbangkan satu contoh lagi (kami memiliki lebih dari 200 kesalahan seperti itu):

 type Player = { name: string; age: number; position: "STRIKER" | "GOALKEEPER", }; type F = () => Promise<Array<Player>>; const f1: F = () => { return Promise.all([ { name: "David Gomes", age: 23, position: "GOALKEEPER", }, { name: "Cristiano Ronaldo", age: 33, position: "STRIKER", } ]); }; 

TypeScript tidak akan memungkinkan Anda untuk menulis ini karena itu tidak akan memungkinkan Anda untuk mendeklarasikan { name: "David Gomes", age: 23, type: "GOALKEEPER" } sebagai objek dengan tipe Player (lihat kotak pasir untuk kesalahan yang tepat). Ini adalah kasus lain di mana saya menemukan TypeScript tidak cukup pintar (setidaknya dibandingkan dengan Flow, yang mengerti kode ini).

Ada beberapa opsi untuk memperbaikinya:

  • Deklarasikan "STRIKER" menjadi "STRIKER" sehingga TypeScript memahami bahwa string adalah enumerasi tipe "STRIKER" | "GOALKEEPER" valid "STRIKER" | "GOALKEEPER" "STRIKER" | "GOALKEEPER" .
  • Deklarasikan semua objek sebagai Player .
  • Atau apa yang saya anggap sebagai solusi terbaik: hanya membantu TypeScript tanpa menggunakan pernyataan tipe apa pun dengan menulis Promise.all<Player>(...) .

Berikut adalah contoh lain (TypeScript) di mana Flow lagi lebih baik di inferensi tipe :

 type Connection = { id: number }; declare function getConnection(): Connection; function resolveConnection() { return new Promise(resolve => { return resolve(getConnection()); }) } resolveConnection().then(conn => { // TypeScript errors in the next line because it does not understand // that conn is of type Connection. We have to manually annotate // resolveConnection as Promise<Connection>. (conn.id); }); 

Contoh yang sangat kecil namun menarik: Flow menganggap Array<T>.pop() tipe T , dan TypeScript menganggapnya sebagai T | void T | void Suatu titik yang mendukung TypeScript, karena memaksa Anda untuk memeriksa ulang keberadaan elemen (jika array kosong, maka Array.pop mengembalikan yang undefined ). Ada beberapa contoh kecil lainnya seperti ini di mana TypeScript lebih unggul dari Flow.

Definisi TypeScript untuk Ketergantungan Pihak Ketiga


Tentu saja, ketika menulis aplikasi JavaScript apa pun, Anda akan memiliki setidaknya beberapa dependensi. Mereka harus diketik, jika tidak, Anda akan kehilangan sebagian besar kemungkinan analisis tipe statis (seperti yang dijelaskan di awal artikel).

Perpustakaan dari npm dapat datang dengan definisi tipe Flow atau TypeScript, dengan atau tanpa keduanya. Sangat sering (kecil) perpustakaan tidak disediakan dengan salah satu atau yang lain, jadi Anda harus menulis definisi jenis Anda sendiri atau meminjamnya dari komunitas. Baik Flow maupun TypeScript mendukung repositori definisi standar untuk paket JavaScript pihak ketiga: ini adalah tipe -diketik dan DefinitelyTyped .

Saya harus mengatakan bahwa DefinitelyTyped kami lebih menyukai. Dengan flow-typed, saya harus menggunakan alat CLI untuk memperkenalkan definisi tipe untuk berbagai dependensi ke dalam proyek. DefinitelyTyped menggabungkan fungsi ini dengan alat CLI npm dengan mengirimkan @types/package-name paket ke repositori paket npm. Ini sangat keren dan sangat menyederhanakan input definisi tipe untuk dependensi kita (gurauan, reaksi, lodash, reaksi-redux, ini hanya beberapa).

Selain itu, saya bersenang-senang mengisi database DefinitelyTyped (jangan berpikir bahwa definisi tipe sama ketika porting kode dari Flow ke TypeScript). Saya sudah mengirim beberapa permintaan tarik , dan tidak ada masalah di mana pun. Cukup tirukan repositori, edit definisi tipe, tambahkan tes - dan kirim permintaan tarik. Bot GitHub DefinitelyTyped menandai pembuat definisi yang Anda edit. Jika tidak ada dari mereka yang memberikan umpan balik dalam 7 hari, permintaan tarik diajukan untuk dipertimbangkan kepada pengelola. Setelah bergabung dengan cabang utama, versi baru dari paket dependensi dikirim ke npm. Misalnya, ketika saya pertama kali memperbarui paket @ types / redux-form, versi 7.4.14 secara otomatis dikirim ke npm. jadi cukup perbarui file package.json untuk mendapatkan definisi tipe baru. Jika Anda tidak bisa menunggu adopsi permintaan tarik, Anda selalu dapat mengubah definisi jenis yang digunakan dalam proyek Anda, seperti yang dijelaskan dalam salah satu artikel sebelumnya .

Secara umum, kualitas definisi tipe di DefinitelyTyped jauh lebih baik karena komunitas TypeScript yang lebih besar dan lebih makmur. Bahkan, setelah proyek dipindahkan ke TypeScript , cakupan tipe kami meningkat dari 88% menjadi 96% , terutama karena definisi yang lebih baik dari tipe ketergantungan pihak ketiga, dengan lebih sedikit jenis any .

Linting dan tes


  1. Kami beralih dari eslint ke tslint (dengan eslint untuk TypeScript, tampaknya lebih sulit untuk memulai).
  2. Tes TypeScript menggunakan ts-jest . Beberapa tes diketik, sementara yang lain tidak (jika diketik terlalu lama, kami menyimpannya sebagai file .js).

Apa yang terjadi setelah memperbaiki semua kesalahan pengetikan?


Setelah 40 jam kerja, kami mencapai kesalahan pengetikan terakhir, menundanya sebentar menggunakan @ts-ignore .

Setelah meninjau komentar ulasan kode dan memperbaiki beberapa bug (sayangnya, saya harus mengubah sedikit kode runtime untuk memperbaiki logika yang tidak dapat dipahami TypeScript) permintaan tariknya hilang, dan sejak itu kami telah menggunakan TypeScript. (Dan ya, kami memperbaiki @ts-ignore pada permintaan tarikan berikutnya).

Selain integrasi dengan editor, bekerja dengan TypeScript sangat mirip dengan bekerja dengan Flow. Kinerja server aliran sedikit lebih tinggi, tetapi ini bukan masalah besar, karena mereka menghasilkan kesalahan untuk file saat ini dengan sama cepat. Satu-satunya perbedaan kinerja adalah bahwa TypeScript melaporkan kesalahan baru setelah menyimpan file beberapa saat kemudian (sekitar 0,5βˆ’1 detik). Waktu startup server kira-kira sama (sekitar 2 menit), tetapi tidak begitu penting. Sejauh ini, kami belum memiliki masalah dengan konsumsi memori. Sepertinya tsc terus menggunakan sekitar 600 MB.

Mungkin tampak bahwa fungsi inferensi tipe memberi Flow keuntungan besar, tetapi ada dua alasan mengapa itu tidak terlalu penting:

  1. Kami mengonversi basis kode aliran ke TypeScript. Jelas, kami hanya menemukan kode yang dapat diekspresikan Flow, tetapi TypeScript tidak. Jika migrasi terjadi di arah yang berlawanan, saya yakin akan ada hal-hal yang ditampilkan / diekspresikan oleh TypeScript.
  2. Ketik inferensi penting dalam membantu menulis kode yang lebih ringkas. Namun semua sama, hal-hal lain lebih penting, seperti komunitas yang kuat dan ketersediaan definisi tipe, karena inferensi tipe lemah dapat diperbaiki dengan menghabiskan sedikit waktu mengetik.

Statistik kode


 $ npm run type-coverage # https://github.com/plantain-00/type-coverage 43330 / 45047 96.19% $ cloc # ignoring tests and dependencies -------------------------------------------------------------------------------- Language files blank comment code -------------------------------------------------------------------------------- TypeScript 330 5179 1405 31463 

Apa selanjutnya


Kami tidak selesai dengan meningkatkan analisis tipe statis. MemSQL memiliki proyek lain yang pada akhirnya akan beralih dari Flow ke TypeScript (dan beberapa proyek JavaScript yang akan mulai menggunakan TypeScript), dan kami ingin membuat konfigurasi TypeScript kami lebih ketat. Saat ini kami mengaktifkan opsi strictNullChecks , tetapi noImplicitAny masih dinonaktifkan. Kami juga akan menghapus beberapa pernyataan tipe berbahaya dari kode.

Saya senang berbagi dengan Anda semua yang saya pelajari selama petualangan saya dengan mengetikkan JavaScript. Jika Anda tertarik pada topik tertentu, beri tahu saya .

Source: https://habr.com/ru/post/id436554/


All Articles