Halo semuanya! Nama saya Dmitry Novikov, saya adalah seorang pengembang javascript di Alfa Bank, dan hari ini saya akan memberi tahu Anda tentang pengalaman kami dalam menurunkan jenis Tindakan menggunakan Typecript, masalah apa yang kami temui dan bagaimana kami menyelesaikannya.
Ini adalah transkrip dari laporan saya tentang Alfa JavaScript MeetUp. Anda dapat melihat kode dari slide presentasi di
sini , dan rekaman mitap disiarkan di
sini .
Aplikasi front-end kami berjalan pada sekelompok React + Redux. Aliran data redux terlihat seperti ini:

Ada pembuat tindakan - fungsi yang mengembalikan aksi. Tindakan jatuh ke peredam, peredam menciptakan sisi baru berdasarkan yang lama. Komponen ditandatangani ke partai, yang pada gilirannya dapat mengirimkan tindakan baru - dan semuanya berulang.
Beginilah tampilan pembuat tindakan dalam kode:

Ini hanya fungsi yang mengembalikan tindakan - objek yang harus memiliki bidang string tipe dan beberapa data (opsional).
Beginilah bentuk peredam yang khas:

Ini adalah kasus sakelar biasa yang melihat bidang jenis tindakan dan menghasilkan sisi baru. Pada contoh di atas, itu hanya menambahkan nilai properti dari tindakan di sana.
Bagaimana jika kita secara tidak sengaja membuat kesalahan dalam menulis peredam? Misalnya, seperti ini, kami akan menukar properti dari tindakan yang berbeda:

Javascript tidak tahu apa-apa tentang tindakan kami dan menganggap kode tersebut benar-benar valid. Namun, itu tidak akan berfungsi sebagaimana dimaksud, dan kami ingin melihat kesalahan ini. Apa yang akan membantu kami jika tidak mengeskripsikan? Mari kita coba melambangkan tindakan kita.

Untuk memulai, kami akan menulis jenis "dahi" untuk tindakan kami - Action1Type dan Action2Type. Dan kemudian, gabungkan mereka menjadi satu jenis penyatuan untuk digunakan dalam peredam. Pendekatannya sederhana dan mudah, tetapi bagaimana jika data dalam tindakan berubah selama pengembangan aplikasi? Jangan mengubah jenis secara manual setiap kali. Kami menulis ulang mereka sebagai berikut:

Operator typeof akan mengembalikan tipe pembuat tindakan kepada kami, dan ReturnType akan memberi kami jenis nilai pengembalian fungsi - yaitu. jenis tindakan. Akibatnya, itu akan berubah sama dengan slide di atas, tetapi tidak lagi secara manual - ketika mengubah tindakan, tipe-tipe ActionType akan diperbarui secara otomatis. Wow! Kami menulisnya di peredam dan ...

Dan segera kami mendapatkan kesalahan dari script. Selain itu, kesalahannya tidak sepenuhnya jelas - properti bar tidak ada dalam tindakan foo, dan foo tidak ada di bar ... Tampaknya memang seperti itu seharusnya? Sesuatu sepertinya kacau. Secara umum, pendekatan dahi tidak bekerja seperti yang diharapkan.
Tapi ini bukan satu-satunya masalah. Bayangkan bahwa seiring waktu, aplikasi kita akan tumbuh, dan kita akan memiliki banyak tindakan. Banyak.

Seperti apa tipe umum kita dalam kasus ini? Mungkin kira-kira seperti ini:

Dan jika kita memperhitungkan bahwa tindakan akan ditambahkan dan dihapus, kita harus mendukung semua ini secara manual - menambah dan menghapus jenis. Ini juga tidak cocok untuk kita sama sekali. Apa yang harus dilakukan Mari kita mulai dengan masalah pertama.

Jadi, kami memiliki beberapa pembuat tindakan, dan jenis yang umum untuk mereka adalah penyatuan jenis tindakan yang diturunkan secara otomatis. Setiap tindakan memiliki properti tipe, dan didefinisikan sebagai string. Ini adalah akar masalahnya. Untuk membedakan satu tindakan dari yang lain, kita perlu setiap jenis menjadi unik dan hanya menerima satu nilai unik.

Tipe ini disebut literal. Jenis literal terdiri dari tiga jenis - numerik, string, dan boolean.

Misalnya, kami memiliki tipe onlyNumberOne dan kami menetapkan bahwa variabel jenis ini hanya bisa sama dengan angka 1. Tetapkan 2 - dan dapatkan kesalahan ketik skrip. String bekerja dengan cara yang sama - hanya satu nilai string spesifik yang dapat ditetapkan ke variabel. Yah, boolean bisa benar atau salah, tanpa ambiguitas.
Generik
Bagaimana cara menyimpan tipe ini tanpa membiarkannya berubah menjadi string? Kami akan menggunakan obat generik. Generik adalah abstraksi atas jenis. Misalkan kita memiliki fungsi yang tidak berguna yang mengambil input sebagai argumen dan mengembalikannya tanpa perubahan. Bagaimana saya bisa mengetiknya? Tulis apa saja, karena ini bisa apa saja jenisnya? Tetapi jika beberapa jenis logika hadir dalam fungsi, maka konversi tipe dapat terjadi, dan, misalnya, angka dapat berubah menjadi string, dan kombinasi apa saja akan melompati ini. Tidak cocok

Seorang generik akan membantu kita keluar dari situasi ini. Entri di atas berarti bahwa kami memberikan argumen tipe T tertentu, dan fungsinya akan mengembalikan tipe yang persis sama dengan T. Kami tidak tahu yang mana yang akan menjadi - nomor, string, boolean atau yang lainnya - tetapi kami dapat menjamin bahwa itu akan menjadi tipe yang persis sama. Opsi ini cocok untuk kita.
Mari kita kembangkan konsep obat generik sedikit. Kita perlu memproses tidak semua jenis secara umum, tetapi string literal yang konkret. Ada kata kunci yang diperluas untuk ini:

Notasi "T extends string" berarti bahwa T adalah tipe tertentu, yang merupakan subset dari tipe string. Perlu dicatat bahwa ini hanya bekerja dengan tipe primitif - jika alih-alih menggunakan string kita akan menggunakan tipe objek dengan set properti tertentu, itu akan sebaliknya berarti bahwa T adalah OVER set dari tipe ini.
Di bawah ini adalah contoh penggunaan fungsi yang diketik dengan extends dan generik:

- Argumen tipe string - fungsi akan mengembalikan string
- Argumen tipe string literal - fungsinya akan mengembalikan string literal
- Jika argumen tidak terlihat seperti string, misalnya, angka, atau array, skrip akan memberikan kesalahan.
Yah, dan secara keseluruhan itu berfungsi.

Kami mengganti fungsi kami dalam jenis tindakan - ia mengembalikan jenis string yang sama persis, tetapi bukan lagi string, tetapi string literal, sebagaimana mestinya. Kami mengumpulkan jenis serikat pekerja, kami mencantumkan peredam - semuanya baik-baik saja. Dan jika kita membuat kesalahan dan menulis properti yang salah, skrip waktu tidak akan memberi kita dua, tetapi satu, kesalahan logis dan dapat dimengerti:

Mari kita melangkah lebih jauh dan abstrak dari tipe string. Kami akan menulis tipifikasi yang sama, hanya menggunakan dua obat generik - T dan U. Sekarang kami memiliki tipe T tertentu yang akan bergantung pada tipe U lainnya, alih-alih kami dapat menggunakan apa saja - setidaknya string, setidaknya angka, setidaknya boolean. Ini diimplementasikan menggunakan fungsi wrapper:

Dan akhirnya: masalah yang diuraikan untuk waktu yang lama sebagai masalah pada github, dan akhirnya, dalam Typecript versi 3.4, para pengembang memberi kami solusi - pernyataan tegas. Ini memiliki dua bentuk rekaman:

Jadi, jika Anda memiliki skrip yang baru, Anda bisa menggunakan salah satu sebagai const dalam tindakan, dan tipe literal tidak akan berubah menjadi string. Di versi yang lebih lama, Anda dapat menggunakan metode yang dijelaskan di atas. Ternyata kami sekarang memiliki dua solusi untuk masalah pertama. Tapi yang kedua tetap.

Kami masih memiliki banyak tindakan berbeda, dan meskipun sekarang kami tahu cara menangani tipenya dengan benar, kami masih tidak tahu cara merakitnya secara otomatis. Kita dapat menulis gabungan secara manual, tetapi jika tindakan dihapus dan ditambahkan, kita masih perlu menghapus secara manual dan menambahkannya dalam jenis. Ini salah.

Di mana untuk memulai? Misalkan kita memiliki pembuat tindakan yang diimpor bersama dari satu file. Kami ingin berkeliling mereka satu per satu, menyimpulkan jenis tindakan mereka dan mengumpulkan mereka menjadi satu jenis serikat pekerja. Dan yang paling penting, kami ingin melakukan ini secara otomatis, tanpa mengedit jenis secara manual.

Mari kita mulai dengan berkeliling pembuat aksi. Untuk melakukan ini, ada tipe dipetakan khusus yang menggambarkan koleksi nilai kunci. Berikut ini sebuah contoh:

Ini menciptakan tipe untuk objek yang kuncinya adalah option1 dan option2 (dari set Keys), dan nilainya benar atau salah. Dalam versi yang lebih umum, ini dapat direpresentasikan sebagai tipe mapOfBool - objek dengan semacam kunci baris dan nilai Boolean.
Bagus Tetapi bagaimana kita dapat memverifikasi bahwa itu adalah objek yang diberikan kepada kita pada input, dan bukan tipe lainnya? Tipe kondisional, sebuah terner sederhana di dunia tipe, akan membantu kita dalam hal ini.

Dalam contoh ini, kita periksa: tipe T memiliki kesamaan dengan string? Jika ya, kembalikan string, dan jika tidak, kembalikan tidak pernah. Ini adalah tipe khusus yang akan selalu mengembalikan kesalahan kepada kami. String literal memenuhi kondisi terner. Berikut ini beberapa contoh kode:

Jika kita menentukan sesuatu dalam generik yang tidak seperti string, naskah akan memberi kita kesalahan.
Kami menemukan solusinya dan verifikasi, tetap hanya untuk mendapatkan jenis dan menggabungkannya ke dalam persatuan. Ini akan membantu kami dengan inferensi tipe kesimpulan dalam naskah. Infer biasanya hidup dalam tipe kondisional, dan melakukan sesuatu seperti ini: ia melewati semua pasangan nilai kunci, mencoba menyimpulkan tipe nilai dan membandingkannya dengan yang lain. Jika jenis nilai berbeda, itu menggabungkan mereka menjadi satu kesatuan. Apa yang kita butuhkan!

Nah, sekarang tinggal menyatukan semuanya.
Ternyata desain ini:

Logikanya kira-kira sebagai berikut: Jika T terlihat seperti objek yang memiliki beberapa kunci string (nama pembuat tindakan), dan mereka memiliki nilai dari beberapa jenis (fungsi yang akan mengembalikan tindakan kepada kami), kemudian mencoba untuk memotong pasangan ini, menyimpulkan jenis nilai-nilai ini dan mengurangi tipe umum mereka. Dan jika ada yang salah - buang kesalahan khusus (ketik tidak pernah).
Sulit hanya pada pandangan pertama. Padahal, semuanya cukup sederhana. Perlu memperhatikan fitur yang menarik - karena fakta bahwa setiap tindakan memiliki bidang jenis yang unik, jenis tindakan ini tidak akan saling menempel, dan kami mendapatkan jenis gabungan penuh pada hasil. Begini tampilannya dalam kode:

Kami mengimpor pembuat tindakan sebagai tindakan, mengambil ReturnType mereka (jenis nilai pengembalian adalah tindakan), dan mengumpulkan menggunakan tipe khusus kami. Ternyata apa yang diminta.

Apa hasilnya? Kami mendapat penyatuan dari tipe literal untuk semua tindakan. Ketika tindakan baru ditambahkan, tipe ini diperbarui secara otomatis. Akibatnya, kami mendapatkan tindakan mengetik yang ketat, sekarang kami tidak dapat melakukan kesalahan. Nah, sepanjang jalan, kami belajar tentang obat generik, tipe kondisional, tipe dipetakan, tidak pernah dan menyimpulkan - Anda dapat memperoleh lebih banyak informasi tentang alat ini di
sini .