Rumah >hujung hadapan web >tutorial js >Cara Menggunakan TypeScript untuk Mengumpul Jenis: Menaip SEMUA kemungkinan pengambilan () Keputusan
Apabila saya mula menulis semula (dengan pasukan saya) aplikasi kami dalam TypeScript dan Svelte (ia dalam JavaScript dan React yang kita semua benci), saya menghadapi masalah:
Bagaimanakah saya boleh menaip semua kemungkinan badan respons HTTP dengan selamat?
Adakah ini membunyikan loceng kepada anda? Jika tidak, kemungkinan besar anda adalah "salah seorang daripada mereka", hehe. Mari kita merenung sejenak untuk memahami gambar dengan lebih baik.
Tiada sesiapa nampaknya mengambil berat tentang "semua kemungkinan badan" respons HTTP, kerana saya tidak dapat menemui apa-apa di luar sana yang telah dibuat untuk ini (baik, mungkin ts-fetch). Biar saya cepat meneliti logik saya di sini tentang mengapa ini berlaku.
Tiada siapa peduli kerana orang juga:
Hanya mengambil berat tentang laluan gembira: Badan respons apabila kod status HTTP ialah 2xx.
Orang ramai menaipnya secara manual di tempat lain.
Untuk #1, saya akan katakan ya, pembangun (terutamanya yang tidak berpengalaman) terlupa bahawa permintaan HTTP mungkin gagal dan maklumat yang dibawa dalam respons yang gagal berkemungkinan besar berbeza dengan respons biasa.
Untuk #2, mari kita kaji isu besar yang terdapat dalam pakej NPM popular seperti ky dan axios.
Setakat yang saya tahu, orang suka pakej seperti ky atau axios kerana salah satu "ciri" mereka ialah mereka membuang ralat pada kod status HTTP bukan OK. Sejak bila ini OK? Sejak tidak pernah. Tetapi nampaknya orang tidak mengambil ini. Orang ramai gembira dan berpuas hati mendapat ralat pada respons yang tidak OK.
Saya membayangkan bahawa orang menaip badan yang tidak OK apabila tiba masanya untuk menangkap. Kecoh, bau kod!
Ini adalah bau kod kerana anda berkesan menggunakan try..catch blocks sebagai branching statement, dan try..catch tidak bermaksud untuk branching statement.
Tetapi walaupun anda berhujah dengan saya bahawa percabangan berlaku secara semula jadi dalam percubaan..tangkap, terdapat satu lagi sebab besar mengapa ini kekal buruk: Apabila ralat dilemparkan, masa jalan perlu melepaskan timbunan panggilan. Ini jauh lebih mahal dari segi kitaran CPU daripada cawangan biasa dengan pernyataan if atau suis.
Mengetahui perkara ini, bolehkah anda mewajarkan prestasi hit hanya untuk menyalahgunakan cubaan..catch block? Saya kata tidak. Saya tidak dapat memikirkan satu pun sebab mengapa dunia hadapan kelihatan sangat gembira dengan ini.
Sekarang saya telah menjelaskan alasan saya, mari kembali kepada topik utama.
Respon HTTP mungkin membawa maklumat yang berbeza bergantung pada kod statusnya. Sebagai contoh, titik akhir todo seperti api/todos/:id yang menerima permintaan HTTP PATCH mungkin mengembalikan respons dengan badan yang berbeza apabila kod status respons ialah 200 berbanding apabila kod status respons ialah 400.
Mari kita contohi:
// For the 200 response, a copy of the updated object: { "id": 123, "text": "The updated text" } // For the 400 response, a list of validation errors: { "errors": [ "The updated text exceeds the maximum allowed number of characters." ] }
Jadi, dengan mengambil kira perkara ini, kita kembali kepada penyataan masalah: Bagaimanakah saya boleh menaip fungsi yang melakukan permintaan PATCH ini di mana TypeScript boleh memberitahu saya badan mana yang saya hadapi, bergantung pada kod status HTTP semasa saya menulis kod? Jawapannya: Gunakan sintaks fasih (sintaks pembina, sintaks berantai) untuk mengumpul jenis.
Mari mulakan dengan menentukan jenis yang dibina berdasarkan jenis sebelumnya:
export type AccumType<T, NewT> = T | NewT;
Sangat mudah: Memandangkan jenis T dan NewT, sertai mereka untuk membentuk jenis baharu. Gunakan jenis baharu ini sebagai T sekali lagi dalam AccumType<>, dan kemudian anda boleh mengumpul satu lagi jenis baharu. Ini dilakukan dengan tangan, bagaimanapun, tidak bagus. Mari perkenalkan satu lagi bahagian penting untuk penyelesaian: Sintaks lancar.
Memandangkan objek kelas X yang kaedahnya sentiasa mengembalikan dirinya (atau salinan dirinya sendiri), seseorang boleh memanggil kaedah rantai satu demi satu. Ini ialah sintaks fasih, atau sintaks berantai.
Mari tulis kelas mudah yang melakukan ini:
export class NamePending<T> { accumulate<NewT>() { return this as NamePending<AccumType<T, NewT>>; } } // Now you can use it like this: const x = new NamePending<{ a: number; }>(); // x is of type NamePending<{ a: number; }>. const y = x.accumulate<{ b: string; }> // y is of type NamePending<{ a: number; } | { b: string; }>.
Eureka! Kami telah berjaya menggabungkan sintaks fasih dan jenis yang kami tulis untuk mula mengumpul jenis data menjadi satu jenis!
Sekiranya ia tidak jelas, anda boleh meneruskan latihan sehingga anda telah mengumpul jenis yang diingini (x.accumulate().accumulate()... sehingga anda selesai).
Ini semua bagus dan bagus, tetapi jenis yang sangat mudah ini tidak mengikat kod status HTTP kepada jenis badan yang sepadan.
Apa yang kami mahukan ialah menyediakan TypeScript maklumat yang mencukupi supaya ciri penyempitan jenisnya bermula. Untuk melakukan ini, mari lakukan perkara yang perlu untuk mendapatkan kod yang berkaitan dengan masalah asal (badan menaip respons HTTP dalam per -asas kod status).
Pertama, namakan semula dan ubah AccumType. Kod di bawah menunjukkan perkembangan dalam lelaran:
// Iteration 1. export type FetchResult<T, NewT> = T | NewT; // Iteration 2. export type FetchResponse<TStatus extends number, TBody> = { ok: boolean; status: TStatus; statusText: string; body: TBody }; export type FetchResult<T, TStatus extends number, NewT> = T | FetchResponse<TStatus, NewT>; //Makes sense to rename NewT to TBody.
Pada ketika ini, saya menyedari sesuatu: Kod status adalah terhad: Saya boleh (dan memang) mencarinya dan menentukan jenis untuknya, dan menggunakan jenis tersebut untuk menyekat parameter jenis TStatus:
// Iteration 3. export type OkStatusCode = 200 | 201 | 202 | ...; export type ClientErrorStatusCode = 400 | 401 | 403 | ...; export type ServerErrorStatusCode = 500 | 501 | 502 | ...; export type StatusCode = OkStatusCode | ClientErrorStatusCode | ServerErrorStatusCode; export type NonOkStatusCode = Exclude<StatusCode, OkStatusCode>; export type FetchResponse<TStatus extends StatusCode, TBody> = { ok: TStatus extends OkStatusCode ? true : false; status: TStatus; statusText: string; body: TBody }; export type FetchResult<T, TStatus extends StatusCode, TBody> = T | FetchResponse<TStatus, TBody>;
Kami telah sampai kepada satu siri jenis yang cantik sahaja: Dengan bercabang (menulis jika pernyataan) berdasarkan syarat pada ok atau sifat status, fungsi penyempitan jenis TypeScript akan bermula! Jika anda tidak percaya, mari tulis bahagian kelas dan cuba:
export class DrFetch<T> { for<TStatus extends StatusCode, TBody>() { return this as DrFetch<FetchResult<T, TStatus, TBody>>; } }
Pandu uji ini:
// For the 200 response, a copy of the updated object: { "id": 123, "text": "The updated text" } // For the 400 response, a list of validation errors: { "errors": [ "The updated text exceeds the maximum allowed number of characters." ] }
Sekarang harus jelas mengapa penyempitan jenis akan dapat meramal bentuk badan dengan betul apabila bercabang, berdasarkan sifat ok bagi sifat status.
Terdapat isu, walau bagaimanapun: Penaipan awal kelas apabila ia dibuat seketika, ditandakan dalam blok ulasan di atas. Saya menyelesaikannya seperti ini:
export type AccumType<T, NewT> = T | NewT;
Perubahan kecil ini berkesan mengecualikan penaipan awal, dan kami kini dalam perniagaan!
Kini kita boleh menulis kod seperti berikut dan Intellisense akan 100% tepat:
export class NamePending<T> { accumulate<NewT>() { return this as NamePending<AccumType<T, NewT>>; } } // Now you can use it like this: const x = new NamePending<{ a: number; }>(); // x is of type NamePending<{ a: number; }>. const y = x.accumulate<{ b: string; }> // y is of type NamePending<{ a: number; } | { b: string; }>.
Penyempitan jenis juga akan berfungsi apabila membuat pertanyaan untuk sifat ok.
Jika anda tidak perasan, kami dapat menulis kod yang lebih baik dengan tidak membuang ralat. Dalam pengalaman profesional saya, axios salah, ky salah, dan mana-mana pembantu pengambilan lain di luar sana melakukan perkara yang sama adalah salah.
TypeScript memang menyeronokkan. Dengan menggabungkan TypeScript dan sintaks fasih, kami dapat mengumpul seberapa banyak jenis yang diperlukan supaya kami boleh menulis kod yang lebih tepat dan lebih jelas dari hari 1, bukan selepas menyahpepijat berulang kali. Teknik ini telah terbukti berjaya dan disiarkan secara langsung untuk dicuba oleh sesiapa sahaja. Pasang dr-fetch dan pandu uji:
// Iteration 1. export type FetchResult<T, NewT> = T | NewT; // Iteration 2. export type FetchResponse<TStatus extends number, TBody> = { ok: boolean; status: TStatus; statusText: string; body: TBody }; export type FetchResult<T, TStatus extends number, NewT> = T | FetchResponse<TStatus, NewT>; //Makes sense to rename NewT to TBody.
Saya juga mencipta wj-config, pakej yang bertujuan untuk menghapuskan sepenuhnya fail .env dan dotenv yang usang. Pakej ini juga menggunakan helah TypeScript yang diajar di sini, tetapi ia menggabungkan jenis dengan &, bukan |. Jika anda ingin mencubanya, pasang v3.0.0-beta.1. Penaipan adalah lebih kompleks, walaupun. Membuat dr-fetch selepas wj-config ialah berjalan-jalan di taman.
Mari kita lihat beberapa ralat di luar sana dalam pakej berkaitan pengambilan.
Anda boleh lihat dalam README ini:
// Iteration 3. export type OkStatusCode = 200 | 201 | 202 | ...; export type ClientErrorStatusCode = 400 | 401 | 403 | ...; export type ServerErrorStatusCode = 500 | 501 | 502 | ...; export type StatusCode = OkStatusCode | ClientErrorStatusCode | ServerErrorStatusCode; export type NonOkStatusCode = Exclude<StatusCode, OkStatusCode>; export type FetchResponse<TStatus extends StatusCode, TBody> = { ok: TStatus extends OkStatusCode ? true : false; status: TStatus; statusText: string; body: TBody }; export type FetchResult<T, TStatus extends StatusCode, TBody> = T | FetchResponse<TStatus, TBody>;
“Sambutan buruk daripada pelayan”? Tidak. "Pelayan mengatakan permintaan anda tidak baik". Ya, bahagian lontaran itu sendiri adalah dahsyat.
Yang ini mempunyai idea yang betul, tetapi malangnya hanya boleh menaip jawapan OK vs bukan OK (maksimum 2 jenis).
Salah satu pakej yang paling saya kritik, menunjukkan contoh ini:
export class DrFetch<T> { for<TStatus extends StatusCode, TBody>() { return this as DrFetch<FetchResult<T, TStatus, TBody>>; } }
Inilah yang akan ditulis oleh pembangun sangat junior: Just the happy path. Persamaan, mengikut READMEnya:
const x = new DrFetch<{}>(); // Ok, having to write an empty type is inconvenient. const y = x .for<200, { a: string; }>() .for<400, { errors: string[]; }>() ; /* y's type: DrFetch<{ ok: true; status: 200; statusText: string; body: { a: string; }; } | { ok: false; status: 400; statusText: string; body: { errors: string[]; }; } | {} // <-------- WHAT IS THIS!!!??? > */
Bahagian lontaran sangat teruk: Mengapa anda bercabang untuk melontar, untuk memaksa anda menangkap kemudian? Ia tidak masuk akal bagi saya. Teks dalam ralat juga mengelirukan: Ia bukan "ralat pengambilan". Pengambilan berjaya. Anda mendapat jawapan, bukan? Anda hanya tidak menyukainya… kerana ia bukan jalan yang menggembirakan. Perkataan yang lebih baik ialah "permintaan HTTP gagal:". Apa yang gagal ialah permintaan itu sendiri, bukan operasi pengambilan.
Atas ialah kandungan terperinci Cara Menggunakan TypeScript untuk Mengumpul Jenis: Menaip SEMUA kemungkinan pengambilan () Keputusan. Untuk maklumat lanjut, sila ikut artikel berkaitan lain di laman web China PHP!