Validasi antarmuka TypeScript menggunakan Joi

Kisah bagaimana menghabiskan dua hari menulis ulang kode yang sama beberapa kali.


Joi & TypeScript. A love story


Entri


Dalam artikel ini, saya akan menghilangkan detail tentang Hapi, Joi, perutean dan validate: { payload: ... } , yang menyiratkan bahwa Anda sudah mengerti tentang apa itu, serta terminologi, ala "antarmuka", "tipe" dan sejenisnya . Saya hanya akan memberi tahu Anda tentang pelatihan berbasis giliran, bukan strategi yang paling sukses, dalam hal ini.


Sedikit latar belakang


Sekarang saya satu-satunya pengembang backend (yaitu, menulis kode) pada proyek. Fungsionalitas bukan esensi, tetapi esensi kuncinya adalah profil yang agak panjang dengan data pribadi. Kecepatan dan kualitas kode didasarkan pada sedikit pengalaman saya bekerja secara mandiri pada proyek-proyek dari awal, lebih sedikit pengalaman bekerja dengan JS (hanya bulan ke-4), dan sepanjang jalan, sangat miring miring, saya menulis dalam TypeScript (selanjutnya - TS). Tanggal dikompresi, gulungan dikompresi, pengeditan terus-menerus tiba dan ternyata untuk menulis kode logika bisnis terlebih dahulu, dan kemudian antarmuka di atas. Namun demikian, tugas teknis mampu mengejar dan mengetuk tutup, yang, kira-kira, telah terjadi pada kita.


Setelah 3 bulan bekerja di proyek, saya akhirnya setuju dengan rekan-rekan saya untuk beralih ke satu kamus sehingga sifat-sifat objek diberi nama dan ditulis sama di mana-mana. Di bawah bisnis ini, tentu saja, saya berusaha untuk menulis antarmuka dan terjebak erat dengannya selama dua hari kerja.


Masalah


Profil pengguna sederhana akan menjadi contoh abstrak.


  • Pertama Langkah nol pengembang yang baik: menggambarkan data menulis tes;
  • Langkah pertama: tulis tes Jelaskan data
  • dan sebagainya.

Misalkan tes sudah ditulis untuk kode ini, tetap untuk menggambarkan data:


 interface IUser { name: string; age: number; phone: string | number; } const aleg: IUser = { name: 'Aleg', age: 45, phone: '79001231212' }; 

Nah, di sini semuanya jelas dan sangat sederhana. Semua kode ini, seperti yang kita ingat, di backend, atau lebih tepatnya, di api, yaitu, pengguna dibuat berdasarkan data yang datang melalui jaringan. Jadi, kita perlu memvalidasi data yang masuk dan membantu Joi dalam hal ini:


 const joiUserValidator = { name: Joi.string(), age: Joi.number(), phone: Joi.alternatives([Joi.string(), Joi.number()]) }; 

Solusi "di dahi" sudah siap. Kelemahan yang jelas dari pendekatan ini adalah bahwa validator benar-benar bercerai dari antarmuka. Jika selama masa aplikasi bidang berubah / tambah atau jenisnya berubah, maka perubahan ini harus dilacak secara manual dan ditunjukkan dalam validator. Saya pikir tidak akan ada pengembang yang bertanggung jawab sampai sesuatu jatuh. Selain itu, dalam proyek kami, kuesioner terdiri dari 50 bidang di tiga tingkat bersarang dan sangat sulit untuk memahami ini, bahkan mengetahui semuanya dengan hati.


Kami tidak bisa menentukan const joiUserValidator: IUser , karena Joi menggunakan tipe datanya, yang menghasilkan kesalahan ketika mengkompilasi tipe Type 'NumberSchema' is not assignable to type 'number' . Tetapi harus ada cara untuk melakukan validasi pada antarmuka?


Mungkin saya tidak melakukan google dengan benar, atau mempelajari jawabannya dengan buruk, tetapi semua keputusan muncul untuk extractTypes dan beberapa jenis sepeda yang sengit, seperti ini :


 type ValidatedValueType<T extends joi.Schema> = T extends joi.StringSchema ? string : T extends joi.NumberSchema ? number : T extends joi.BooleanSchema ? boolean : T extends joi.ObjectSchema ? ValidatedObjectType<T> : /* ... more schemata ... */ never; 

Solusi


Gunakan perpustakaan pihak ketiga


Kenapa tidak Ketika saya bertanya kepada orang-orang tentang tugas saya, saya menerima salah satu jawaban, dan kemudian, dan di sini, di komentar (terima kasih kepada keenondrums ), tautan ke perpustakaan ini:
https://github.com/typestack/class-validator
https://github.com/typestack/class-transformer


Namun, ada minat untuk mengetahuinya sendiri, untuk lebih memahami pekerjaan TS, dan tidak ada yang mendesak untuk menyelesaikan masalah sejenak.


Dapatkan semua properti


Karena saya tidak pernah bekerja dengan statika sebelumnya, kode di atas menemukan Amerika dalam hal menggunakan operator ternary dalam jenis. Untungnya, tidak mungkin untuk menerapkannya dalam proyek. Tetapi saya menemukan sepeda lain yang menarik:


 interface IUser { name: string; age: number; phone: string | number; } type UserKeys<T> = { [key in keyof T]; } const evan: UserKeys<IUser> = { name: 'Evan', age: 32, phone: 791234567890 }; const joiUser: UserKeys<IUser> = { name: Joi.string(), age: Joi.number(), phone: Joi.alternatives([Joi.string(), Joi.number()]) }; 

TypeScript bawah kondisi yang agak rumit dan misterius memungkinkan Anda untuk mendapatkan, misalnya, kunci dari antarmuka, seolah-olah itu adalah objek JS normal, namun, hanya dalam konstruksi type dan melalui key in keyof T dan hanya melalui generik. Sebagai hasil dari tipe UserKeys , semua objek yang mengimplementasikan antarmuka harus memiliki set properti yang sama, tetapi tipe nilai bisa berubah-ubah. Ini termasuk petunjuk dalam IDE, tetapi masih tidak memberikan indikasi yang jelas tentang jenis nilai.


Ini kasus menarik lainnya yang tidak bisa saya gunakan. Mungkin Anda bisa memberi tahu saya mengapa ini perlu (walaupun saya kira sebagian, tidak ada cukup contoh yang diterapkan):


 interface IUser { name: string; age: number; phone: string | number; } interface IUserJoi { name: Joi.StringSchema, age: Joi.NumberSchema, phone: Joi.AlternativesSchema } type UserKeys<T> = { [key in keyof T]: T[key]; } const evan: UserKeys<IUser> = { name: 'Evan', age: 32, phone: 791234567890 }; const userJoiValidator: UserKeys<IUserJoi> = { name: Joi.string(), age: Joi.number(), phone: Joi.alternatives([Joi.string(), Joi.number()]) }; 

Gunakan tipe variabel


Anda dapat secara eksplisit mengatur jenis, dan menggunakan "ATAU" dan mengekstraksi properti, dapatkan kode yang berfungsi secara lokal:


 type TString = string | Joi.StringSchema; type TNumber = number | Joi.NumberSchema; type TStdAlter = TString | TNumber; type TAlter = TStdAlter | Joi.AlternativesSchema; export interface IUser { name: TString; age: TNumber; phone: TAlter; } type UserKeys<T> = { [key in keyof T]; } const olex: UserKeys<IUser> = { name: 'Olex', age: 67, phone: '79998887766' }; const joiUser: UserKeys<IUser> = { name: Joi.string(), age: Joi.number(), phone: Joi.alternatives([Joi.string(), Joi.number()]) }; 

Masalah kode ini dimanifestasikan ketika kita ingin mengambil objek yang valid, misalnya, dari database, yaitu, TS tidak tahu sebelumnya jenis data apa yang akan - sederhana atau Joi. Ini dapat menyebabkan kesalahan ketika mencoba melakukan operasi matematika pada bidang yang diharapkan sebagai number :


 const someUser: IUser = getUserFromDB({ name: 'Aleg' }); const someWeirdMath = someUser.age % 10; // error TS2362: The left-hand side of an arithmetic operation must be of type'any', 'number', 'bigint' or an enum type 

Kesalahan ini berasal dari Joi.NumberSchema karena usia bisa bukan hanya number . Apa yang mereka perjuangkan dan temui.


Menggabungkan dua solusi menjadi satu?


Di suatu tempat pada titik ini, hari kerja mendekati kesimpulan logisnya. Aku menghela nafas, minum kopi, dan menghapus semua itu. Anda perlu membaca lebih sedikit Internet ini! Waktunya telah tiba ambil senapan dan cuci otak:


  1. Objek harus dibentuk dengan tipe nilai eksplisit;
  2. Anda bisa menggunakan obat generik untuk membuang tipe ke satu antarmuka;
  3. Generik mendukung tipe standar;
  4. type konstruksi jelas mampu melakukan hal lain.

Kami menulis antarmuka generik dengan tipe default:


 interface IUser < TName = string, TAge = number, TAlt = string | number > { name: TName; age: TAge; phone: TAlt; } 

Untuk Joi, Anda bisa membuat antarmuka kedua, mewarisi antarmuka utama dengan cara ini:


 interface IUserJoi extends IUser < Joi.StringSchema, Joi.NumberSchema, Joi.AlternativesSchema > {} 

Tidak cukup baik, karena pengembang selanjutnya dapat memperluas IUserJoi dengan hati yang ringan atau lebih buruk. Opsi yang lebih terbatas adalah untuk mendapatkan perilaku serupa:


 type IUserJoi = IUser<Joi.StringSchema, Joi.NumberSchema, Joi.AlternativesSchema>; 

Kami mencoba:


 const aleg: IUser = { name: 'Aleg', age: 45, phone: '79001231212' }; const joiUser: IUserJoi = { name: Joi.string(), age: Joi.number(), phone: Joi.alternatives([Joi.string(), Joi.number()]) }; 

UPD:
Untuk menyelesaikan di Joi.object saya harus berjuang dengan kesalahan TS2345 dan solusi paling sederhana adalah as any . Saya pikir ini bukan asumsi kritis, karena objek di atas masih ada di antarmuka.


 const joiUserInfo = { info: Joi.object(joiUser as any).required() }; 

Itu mengkompilasi, terlihat rapi di tempat penggunaan, dan tanpa adanya kondisi khusus selalu menetapkan jenis default! Kecantikan ...
-
... apa yang saya habiskan dua hari kerja


Ringkasan


Kesimpulan apa yang bisa ditarik dari semua ini:


  1. Jelas, saya tidak belajar bagaimana menemukan jawaban atas pertanyaan. Tentunya, dengan permintaan yang berhasil, solusi ini (atau bahkan lebih baik) ada di tautan 5k pertama dari mesin pencari;
  2. Beralih ke pemikiran statis dari dinamis tidak begitu mudah, lebih sering saya hanya palu di kerumunan seperti itu;
  3. Generik itu keren. Pada Habr dan stackoverflow penuh sepeda solusi tidak jelas untuk membangun pengetikan yang kuat ... di luar runtime.

Apa yang kami menangkan:


  1. Saat mengubah antarmuka, semua kode jatuh, termasuk validator;
  2. Di editor, kiat muncul pada nama properti dan jenis nilai objek untuk menulis validator;
  3. Kurangnya perpustakaan pihak ketiga yang tidak jelas untuk tujuan yang sama;
  4. Aturan Joi hanya akan diterapkan jika perlu, dalam kasus lain, tipe standar;
  5. Jika seseorang ingin mengubah tipe nilai properti, maka dengan organisasi kode yang benar, ia akan pergi ke tempat semua tipe yang terkait dengan properti ini dikumpulkan bersama;
  6. Kami belajar untuk dengan indah dan hanya menyembunyikan obat generik di belakang abstraksi type , secara visual menurunkan kode dari konstruksi mengerikan.

Moral: Pengalaman tak ternilai, untuk sisanya, ada peta Dunia.


Anda dapat melihat, menyentuh, menjalankan hasil akhir:
https://repl.it/@Melodyn/Joi-by-interface

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


All Articles