TypeScript School of Magic: Generik dan Jenis Ekstensi

Penulis artikel yang kami terjemahkan hari ini mengatakan TypeScript is awesome. Ketika pertama kali mulai menggunakan TS, ia sangat menyukai kebebasan yang melekat dalam bahasa ini. Semakin banyak upaya yang dilakukan seorang programmer dalam pekerjaannya dengan mekanisme TS-spesifik, semakin besar manfaat yang akan diterimanya. Kemudian ia menggunakan tipe anotasi hanya secara berkala. Kadang-kadang ia menggunakan peluang untuk penyelesaian kode dan petunjuk kompiler, tetapi terutama hanya mengandalkan visinya sendiri tentang tugas-tugas yang diselesaikannya.

Seiring waktu, penulis materi ini menyadari bahwa setiap kali ia mem-bypass kesalahan yang terdeteksi pada tahap kompilasi, ia meletakkan bom waktu dalam kodenya yang dapat meledak selama eksekusi program. Setiap kali dia "berjuang" dengan kesalahan menggunakan konstruksi sederhana as any , dia harus membayarnya dengan berjam-jam hard debugging.



Akibatnya, ia sampai pada kesimpulan bahwa lebih baik tidak melakukannya. Dia berteman dengan kompiler, mulai memperhatikan petunjuknya. Kompiler menemukan masalah dalam kode dan melaporkannya jauh sebelum mereka dapat menyebabkan kerusakan nyata. Penulis artikel, yang memandang dirinya sebagai pengembang, menyadari bahwa kompiler adalah sahabatnya, karena melindungi dirinya dari dirinya sendiri. Bagaimana seseorang tidak dapat mengingat kata-kata Albus Dumbledore: "Dibutuhkan banyak keberanian untuk berbicara melawan musuhmu, tetapi tidak kurang dari itu diperlukan untuk berbicara melawan temanmu."

Tidak peduli seberapa bagus kompilernya, tidak selalu mudah untuk menyenangkannya. Terkadang menghindari penggunaan jenis any sangat sulit. Dan kadang-kadang tampaknya any solusi satu-satunya yang masuk akal untuk beberapa masalah.

Materi ini berfokus pada dua situasi. Dengan menghindari penggunaan jenis any di dalamnya, Anda dapat memastikan keamanan jenis kode, membuka kemungkinan untuk digunakan kembali, dan membuatnya intuitif.

Generik


Misalkan kita sedang mengerjakan basis data sebuah sekolah. Kami menulis fungsi pembantu yang sangat nyaman, getBy . Untuk mendapatkan objek yang mewakili siswa dengan namanya, kita dapat menggunakan perintah dari form getBy(model, "name", "Harry") . Mari kita lihat implementasi mekanisme ini (di sini, agar tidak menyulitkan kode, database diwakili oleh array biasa).

 type Student = { name: string; age: number; hasScar: boolean; }; const students: Student[] = [ { name: "Harry", age: 17, hasScar: true }, { name: "Ron", age: 17, hasScar: false }, { name: "Hermione", age: 16, hasScar: false } ]; function getBy(model, prop, value) {   return model.filter(item => item[prop] === value)[0] } 

Seperti yang Anda lihat, kami memiliki fungsi yang baik, tetapi tidak menggunakan anotasi jenis, dan ketidakhadirannya juga berarti bahwa fungsi semacam itu tidak dapat disebut tipe-aman. Perbaiki

 function getBy(model: Student[], prop: string, value): Student | null {   return model.filter(item => item[prop] === value)[0] || null } const result = getBy(students, "name", "Hermione") // result: Student 

Jadi fungsi kita sudah terlihat jauh lebih baik. Kompiler sekarang tahu jenis hasil yang diharapkan darinya, ini akan berguna nanti. Namun, untuk mencapai pekerjaan yang aman dengan tipe, kami mengorbankan kemungkinan menggunakan kembali fungsi. Bagaimana jika kita perlu menggunakannya untuk mendapatkan entitas lain? Tidak mungkin fungsi ini tidak dapat ditingkatkan dengan cara apa pun. Dan memang benar.

Dalam TypeScript, seperti dalam bahasa yang sangat diketik lainnya, kita dapat menggunakan generik, yang juga disebut "tipe generik", "tipe universal", "generalisasi".

Sebuah generik mirip dengan variabel biasa, tetapi alih-alih beberapa nilai, itu berisi definisi tipe. Kami menulis ulang kode fungsi kami sehingga alih-alih tipe Student itu akan menggunakan tipe universal T

 function getBy<T>(model: T[], prop: string, value): T | null {   return model.filter(item => item[prop] === value)[0] } const result = getBy<Student>(students, "name", "Hermione") // result: Student 

Cantik! Sekarang fungsi ini ideal untuk digunakan kembali, sementara keamanan jenis masih di pihak kita. Perhatikan bagaimana tipe Student secara eksplisit diatur di baris terakhir dari potongan kode di atas di mana T generik T . Hal ini dilakukan untuk membuat contoh sejelas mungkin, tetapi kompiler, pada kenyataannya, secara mandiri dapat menurunkan tipe yang diperlukan, jadi dalam contoh berikut ini kami tidak akan melakukan penyempurnaan tipe tersebut.

Jadi sekarang kami memiliki fungsi pembantu yang andal, cocok untuk digunakan kembali. Namun, itu masih bisa diperbaiki. Bagaimana jika kesalahan dibuat ketika memasukkan parameter kedua dan bukannya "name" tampaknya ada "naem" ? Fungsi tersebut akan berperilaku seolah-olah siswa yang Anda cari sama sekali tidak ada dalam database, dan, yang paling tidak menyenangkan, itu tidak akan menghasilkan kesalahan. Ini dapat menghasilkan debugging jangka panjang.

Untuk melindungi dari kesalahan semacam itu, kami memperkenalkan tipe universal lain, P Dalam hal ini, perlu bahwa P menjadi kunci dari tipe T , oleh karena itu, jika Student digunakan di sini, maka perlu bahwa P menjadi string "name" , "age" atau "hasScar" . Begini cara melakukannya.

 function getBy<T, P extends keyof T>(model: T[], prop: P, value): T | null {   return model.filter(item => item[prop] === value)[0] || null } const result = getBy(students, "naem", "Hermione") // Error: Argument of type '"naem"' is not assignable to parameter of type '"name" | "age" | "hasScar"'. 

Menggunakan obat generik dan keyof adalah trik yang sangat kuat. Jika Anda menulis program dalam IDE yang mendukung TypeScript, maka dengan memasukkan argumen, Anda dapat memanfaatkan kapabilitas pelengkapan otomatis, yang sangat nyaman.

Namun, kami belum selesai mengerjakan fungsi getBy . Dia memiliki argumen ketiga, jenis yang belum kita tentukan. Ini sama sekali tidak cocok untuk kita. Sampai sekarang, kita tidak bisa tahu sebelumnya apa yang seharusnya, karena itu tergantung pada apa yang kita lewati sebagai argumen kedua. Tetapi sekarang, karena kita memiliki tipe P , kita dapat secara dinamis menyimpulkan tipe untuk argumen ketiga. Jenis argumen ketiga pada akhirnya adalah T[P] . Akibatnya, jika T adalah Student , dan P adalah "age" , maka T[P] akan menjadi number tipe.

 function getBy<T, P extends keyof T>(model: T[], prop: P, value: T[P]): T | null {   return model.filter(item => item[prop] === value)[0] || null } const result = getBy(students, "age", "17") // Error: Argument of type '"17"' is not assignable to parameter of type 'number'. const anotherResult = getBy(students, "hasScar", "true") // Error: Argument of type '"true"' is not assignable to parameter of type 'boolean'. const yetAnotherResult = getBy(students, "name", "Harry") //      

Saya harap sekarang Anda memiliki pemahaman yang sangat jelas tentang cara menggunakan obat generik dalam TypeScript, tetapi jika Anda ingin bereksperimen dengan sangat baik dengan semua yang Anda ingin bereksperimen dengan kode yang dibahas di sini, Anda dapat melihatnya di sini .

Memperluas jenis yang ada


Kadang-kadang kita mungkin mengalami kebutuhan untuk menambahkan data atau fungsionalitas ke antarmuka yang kodenya tidak dapat kita ubah. Anda mungkin perlu mengubah objek standar, katakan - tambahkan beberapa properti ke objek window , atau memperpanjang perilaku beberapa perpustakaan eksternal seperti Express . Dan dalam kedua kasus, Anda tidak memiliki kemampuan untuk secara langsung mempengaruhi objek yang ingin Anda gunakan.

Kami akan melihat solusi untuk masalah ini dengan menambahkan fungsi getBy yang sudah Anda ketahui ke prototipe Array . Ini akan memungkinkan kami, menggunakan fungsi ini, untuk membangun konstruksi sintaksis yang lebih akurat. Saat ini, kita tidak berbicara tentang apakah baik atau buruk untuk memperluas objek standar, karena tujuan utama kami adalah mempelajari pendekatan yang sedang dipertimbangkan.

Jika kita mencoba menambahkan fungsi ke prototipe Array , kompiler tidak akan terlalu menyukai ini:

 Array.prototype.getBy = function <T, P extends keyof T>(   this: T[],   prop: P,   value: T[P] ): T | null { return this.filter(item => item[prop] === value)[0] || null; }; // Error: Property 'getBy' does not exist on type 'any[]'. const bestie = students.getBy("name", "Ron"); // Error: Property 'getBy' does not exist on type 'Student[]'. const potionsTeacher = (teachers as any).getBy("subject", "Potions") //  ...   ? 

Jika kami mencoba meyakinkan kompilator dengan menggunakan konstruk as any secara berkala, kami akan membatalkan semua yang telah kami capai. Kompiler akan diam, tetapi Anda bisa melupakan pekerjaan aman dengan tipe.

Akan lebih baik untuk memperluas jenis Array , tetapi sebelum melakukan ini, mari kita bicara tentang bagaimana TypeScript menangani situasi ketika dua antarmuka dari jenis yang sama hadir dalam kode. Di sini skema tindakan sederhana diterapkan. Iklan akan, jika mungkin, digabungkan. Jika Anda tidak dapat menggabungkannya, sistem akan memberikan kesalahan.

Jadi kode ini berfungsi:

 interface Wand { length: number } interface Wand {   core: string } const myWand: Wand = { length: 11, core: "phoenix feather" } //  ! 

Dan yang ini bukan:

 interface Wand { length: number } interface Wand {   length: string } // Error: Subsequent property declarations must have the same type.  Property 'length' must be of type 'number', but here has type 'string'. 

Sekarang, setelah berurusan dengan ini, kita melihat bahwa kita dihadapkan dengan tugas yang agak sederhana. Yaitu, yang perlu kita lakukan adalah mendeklarasikan antarmuka Array<T> dan menambahkan fungsi getBy ke dalamnya.

 interface Array<T> {  getBy<P extends keyof T>(prop: P, value: T[P]): T | null; } Array.prototype.getBy = function <T, P extends keyof T>(   this: T[],   prop: P,   value: T[P] ): T | null { return this.filter(item => item[prop] === value)[0] || null; }; const bestie = students.getBy("name", "Ron"); //   ! const potionsTeacher = (teachers as any).getBy("subject", "Potions") //     

Harap dicatat bahwa sebagian besar kode yang cenderung Anda tulis dalam file modul, oleh karena itu, untuk membuat perubahan pada antarmuka Array , Anda akan memerlukan akses ke lingkup global. Anda dapat melakukan ini dengan menempatkan definisi tipe di dalam declare global . Misalnya, seperti ini:

 declare global {   interface Array<T> {       getBy<P extends keyof T>(prop: P, value: T[P]): T | null;   } } 

Jika Anda akan memperluas antarmuka perpustakaan eksternal, maka kemungkinan besar Anda akan memerlukan akses ke namespace perpustakaan ini. Berikut adalah contoh cara menambahkan bidang userId ke Request dari perpustakaan Express :

 declare global { namespace Express {   interface Request {     userId: string;   } } } 

Anda dapat bereksperimen dengan kode di bagian ini di sini .

Ringkasan


Pada artikel ini, kami melihat teknik untuk menggunakan generik dan mengetik ekstensi dalam TypeScript. Kami berharap apa yang Anda pelajari hari ini akan membantu Anda menulis kode yang andal, mudah dimengerti, dan aman.

Pembaca yang budiman! Bagaimana perasaan Anda tentang semua jenis dalam TypeScript?

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


All Articles