Jelajahi cara mencapai pencocokan pola yang aman tipe, diverifikasi waktu kompilasi di JavaScript menggunakan TypeScript, gabungan yang didiskriminasikan, dan pustaka modern untuk menulis kode yang tangguh, bebas bug.
Pencocokan Pola & Keamanan Tipe JavaScript: Panduan Verifikasi Waktu Kompilasi
Pencocokan pola adalah salah satu fitur paling kuat dan ekspresif dalam pemrograman modern, yang telah lama dirayakan dalam bahasa fungsional seperti Haskell, Rust, dan F#. Fitur ini memungkinkan pengembang untuk membongkar data dan mengeksekusi kode berdasarkan strukturnya dengan cara yang ringkas dan sangat mudah dibaca. Seiring JavaScript terus berkembang, pengembang semakin ingin mengadopsi paradigma yang kuat ini. Namun, tantangan signifikan tetap ada: Bagaimana kita mencapai keamanan tipe dan jaminan waktu kompilasi yang kuat dari bahasa-bahasa ini di dunia dinamis JavaScript?
Jawabannya terletak pada pemanfaatan sistem tipe statis TypeScript. Meskipun JavaScript sendiri selangkah lebih maju menuju pencocokan pola asli, sifat dinamisnya berarti setiap pemeriksaan akan terjadi saat runtime, yang berpotensi menyebabkan kesalahan tak terduga dalam produksi. Artikel ini adalah penyelaman mendalam ke dalam teknik dan alat yang memungkinkan verifikasi pencocokan waktu kompilasi yang sebenarnya, memastikan bahwa Anda menangkap kesalahan bukan saat pengguna Anda melakukannya, tetapi saat Anda mengetik.
Kita akan menjelajahi cara membangun sistem yang tangguh, mendokumentasikan diri sendiri, dan tahan terhadap kesalahan dengan menggabungkan fitur-fitur canggih TypeScript dengan keanggunan pencocokan pola. Bersiaplah untuk menghilangkan seluruh kelas bug runtime dan menulis kode yang lebih aman dan lebih mudah dipelihara.
Apa Sebenarnya Pencocokan Pola Itu?
Pada intinya, pencocokan pola adalah mekanisme kontrol alur yang canggih. Ini seperti pernyataan `switch` yang sangat canggih. Alih-alih hanya memeriksa kesetaraan terhadap nilai sederhana (seperti angka atau string), pencocokan pola memungkinkan Anda memeriksa nilai terhadap 'pola' yang kompleks dan, jika kecocokan ditemukan, mengikat variabel ke bagian dari nilai tersebut.
Mari kita bandingkan dengan pendekatan tradisional:
Cara Lama: Rantai `if-else` dan `switch`
Pertimbangkan fungsi yang menghitung luas bentuk geometris. Dengan pendekatan tradisional, kode Anda mungkin terlihat seperti ini:
// Bentuk adalah objek dengan properti 'type'
function calculateArea(shape) {
if (shape.type === 'circle') {
return Math.PI * shape.radius * shape.radius;
} else if (shape.type === 'square') {
return shape.sideLength * shape.sideLength;
} else if (shape.type === 'rectangle') {
return shape.width * shape.height;
} else {
throw new Error('Unsupported shape type');
}
}
Ini berhasil, tetapi bertele-tele dan rawan kesalahan. Bagaimana jika Anda menambahkan bentuk baru, seperti `triangle`, tetapi lupa memperbarui fungsi ini? Kode akan melemparkan kesalahan generik saat runtime, yang mungkin jauh dari tempat bug sebenarnya diperkenalkan.
Cara Pencocokan Pola: Deklaratif dan Ekspresif
Pencocokan pola membingkai ulang logika ini agar lebih deklaratif. Alih-alih serangkaian pemeriksaan imperatif, Anda mendeklarasikan pola yang Anda harapkan dan tindakan yang akan diambil:
// Pseudocode untuk fitur pencocokan pola JavaScript di masa mendatang
function calculateArea(shape) {
match (shape) {
when ({ type: 'circle', radius }): return Math.PI * radius * radius;
when ({ type: 'square', sideLength }): return sideLength * sideLength;
when ({ type: 'rectangle', width, height }): return width * height;
default: throw new Error('Unsupported shape type');
}
}
Manfaat utama langsung terlihat:
- Destructuring: Nilai seperti `radius`, `width`, dan `height` secara otomatis diekstraksi dari objek `shape`.
- Keterbacaan: Maksud kode lebih jelas. Setiap klausul `when` menjelaskan struktur data tertentu dan logika yang sesuai.
- Kelengkapan: Ini adalah manfaat paling penting untuk keamanan tipe. Sistem pencocokan pola yang benar-benar tangguh dapat memberi tahu Anda saat waktu kompilasi jika Anda lupa menangani kemungkinan kasus. Ini adalah tujuan utama kita.
Tantangan JavaScript: Dinamisme vs. Keamanan
Kekuatan terbesar JavaScript—fleksibilitas dan sifat dinamisnya—juga merupakan kelemahan terbesarnya dalam hal keamanan tipe. Tanpa sistem tipe statis yang menegakkan kontrak saat waktu kompilasi, pencocokan pola dalam JavaScript biasa terbatas pada pemeriksaan runtime. Ini berarti:
- Tanpa Jaminan Waktu Kompilasi: Anda tidak akan tahu bahwa Anda melewatkan sebuah kasus sampai kode Anda berjalan dan mencapai jalur spesifik tersebut.
- Kegagalan Diam-diam: Jika Anda lupa kasus default, nilai yang tidak cocok mungkin hanya menghasilkan `undefined`, menyebabkan bug halus ke hilir.
- Malam Refaktorisasi: Menambahkan varian baru ke struktur data (misalnya, jenis peristiwa baru, status respons API baru) memerlukan pencarian dan penggantian global untuk menemukan semua tempat yang perlu ditangani. Melewatkan satu dapat merusak aplikasi Anda.
Di sinilah TypeScript benar-benar mengubah permainan. Sistem tipe statisnya memungkinkan kita untuk memodelkan data kita secara tepat dan kemudian memanfaatkan kompiler untuk menegakkan bahwa kita menangani setiap variasi yang mungkin. Mari kita jelajahi caranya.
Teknik 1: Fondasi dengan Gabungan yang Didiskriminasikan
Fitur TypeScript yang paling penting untuk memungkinkan pencocokan pola yang aman tipe adalah gabungan yang didiskriminasikan (juga dikenal sebagai gabungan bertag atau tipe data aljabar). Ini adalah cara yang ampuh untuk memodelkan tipe yang dapat menjadi salah satu dari beberapa kemungkinan yang berbeda.
Apa itu Gabungan yang Didiskriminasikan?
Gabungan yang didiskriminasikan dibangun dari tiga komponen:
- Serangkaian tipe yang berbeda (anggota gabungan).
- Properti umum dengan tipe literal, yang dikenal sebagai diskriminan atau tag. Properti ini memungkinkan TypeScript untuk mempersempit tipe spesifik dalam gabungan.
- Tipe gabungan yang menggabungkan semua tipe anggota.
Mari kita model ulang contoh bentuk kita menggunakan pola ini:
// 1. Tentukan tipe anggota yang berbeda
interface Circle {
kind: 'circle'; // Diskriminan
radius: number;
}
interface Square {
kind: 'square'; // Diskriminan
sideLength: number;
}
interface Rectangle {
kind: 'rectangle'; // Diskriminan
width: number;
height: number;
}
// 2. Buat tipe gabungan
type Shape = Circle | Square | Rectangle;
Sekarang, variabel dengan tipe `Shape` harus menjadi salah satu dari tiga antarmuka ini. Properti `kind` bertindak sebagai kunci yang membuka kemampuan penyempitan tipe TypeScript.
Menerapkan Pemeriksaan Kelengkapan Waktu Kompilasi
Dengan gabungan yang didiskriminasikan di tempatnya, kita sekarang dapat menulis fungsi yang dijamin oleh kompiler untuk menangani setiap kemungkinan bentuk. Bahan ajaibnya adalah tipe `never` TypeScript, yang mewakili nilai yang seharusnya tidak pernah terjadi.
Kita dapat menulis fungsi pembantu sederhana untuk menegakkan ini:
function assertUnreachable(x: never): never {
throw new Error("Didn't expect to get here");
}
Sekarang, mari kita tulis ulang fungsi `calculateArea` kita menggunakan pernyataan `switch` standar. Perhatikan apa yang terjadi di blok `default`:
function calculateArea(shape: Shape): number {
switch (shape.kind) {
case 'circle':
// TypeScript tahu `shape` adalah Circle di sini!
return Math.PI * shape.radius ** 2;
case 'square':
// TypeScript tahu `shape` adalah Square di sini!
return shape.sideLength ** 2;
case 'rectangle':
// TypeScript tahu `shape` adalah Rectangle di sini!
return shape.width * shape.height;
default:
// Jika kita telah menangani semua kasus, `shape` akan bertipe `never`
return assertUnreachable(shape);
}
}
Kode ini dikompilasi dengan sempurna. Di dalam setiap blok `case`, TypeScript telah mempersempit tipe `shape` menjadi `Circle`, `Square`, atau `Rectangle`, memungkinkan kita untuk mengakses properti seperti `radius` dengan aman.
Sekarang untuk momen ajaib. Mari kita perkenalkan bentuk baru ke sistem kita:
interface Triangle {
kind: 'triangle';
base: number;
height: number;
}
type Shape = Circle | Square | Rectangle | Triangle; // Tambahkan ke gabungan
Segera setelah kita menambahkan `Triangle` ke gabungan `Shape`, fungsi `calculateArea` kita akan segera menghasilkan kesalahan waktu kompilasi:
// Di blok `default` dari `calculateArea`:
return assertUnreachable(shape);
// ~~~~~
// Argumen tipe 'Triangle' tidak dapat ditetapkan ke parameter tipe 'never'.
Kesalahan ini sangat berharga. Kompiler TypeScript memberi tahu kita, "Anda berjanji untuk menangani setiap `Shape` yang mungkin, tetapi Anda lupa `Triangle`. Variabel `shape` masih bisa menjadi `Triangle` dalam kasus default, dan itu tidak dapat ditetapkan ke `never`."
Untuk memperbaiki kesalahan, kita cukup menambahkan kasus yang hilang. Kompiler menjadi jaring pengaman kita, menjamin bahwa logika kita tetap sinkron dengan model data kita.
// ... di dalam switch
case 'triangle':
return 0.5 * shape.base * shape.height;
default:
return assertUnreachable(shape);
// ... sekarang kode berhasil dikompilasi lagi!
Kelebihan dan Kekurangan Pendekatan Ini
- Kelebihan:
- Tanpa Dependensi: Hanya menggunakan fitur inti TypeScript.
- Keamanan Tipe Maksimum: Memberikan jaminan waktu kompilasi yang kuat.
- Kinerja Sangat Baik: Dikompilasi menjadi pernyataan `switch` JavaScript standar yang sangat dioptimalkan.
- Kekurangan:
- Bertele-tele: Boilerplate `switch`, `case`, `break`/`return`, dan `default` bisa terasa merepotkan.
- Bukan Ekspresi: Pernyataan `switch` tidak dapat langsung dikembalikan atau ditetapkan ke variabel, yang mengarah ke gaya kode yang lebih imperatif.
Teknik 2: API Ergonomis dengan Pustaka Modern
Meskipun gabungan yang didiskriminasikan dengan pernyataan `switch` adalah fondasinya, boilerplate-nya bisa melelahkan. Hal ini telah mendorong munculnya pustaka sumber terbuka yang fantastis yang menyediakan API yang lebih fungsional, ekspresif, dan ergonomis untuk pencocokan pola, sambil tetap memanfaatkan kompiler TypeScript untuk keamanan.
Memperkenalkan `ts-pattern`
Salah satu pustaka paling populer dan kuat dalam hal ini adalah `ts-pattern`. Ini memungkinkan Anda mengganti pernyataan `switch` dengan API yang lancar dan dapat dirangkai yang berfungsi sebagai ekspresi.
Mari kita tulis ulang fungsi `calculateArea` kita menggunakan `ts-pattern`:
import { match } from 'ts-pattern';
function calculateAreaWithTsPattern(shape: Shape): number {
return match(shape)
.with({ kind: 'circle' }, (s) => Math.PI * s.radius ** 2)
.with({ kind: 'square' }, (s) => s.sideLength ** 2)
.with({ kind: 'rectangle' }, (s) => s.width * s.height)
.with({ kind: 'triangle' }, (s) => 0.5 * s.base * s.height)
.exhaustive(); // Ini adalah kunci keamanan waktu kompilasi
}
Mari kita uraikan apa yang terjadi:
- `match(shape)`: Ini memulai ekspresi pencocokan pola, mengambil nilai yang akan dicocokkan.
- `.with({ kind: '...' }, handler)`: Setiap panggilan `.with()` mendefinisikan pola. `ts-pattern` cukup pintar untuk menyimpulkan tipe argumen kedua (fungsi `handler`). Untuk pola `{ kind: 'circle' }`, ia tahu input `s` ke handler akan bertipe `Circle`.
- `.exhaustive()`: Metode ini setara dengan trik `assertUnreachable` kita. Ini memberi tahu `ts-pattern` bahwa semua kasus yang mungkin harus ditangani. Jika kita menghapus baris `.with({ kind: 'triangle' }, ...)` , `ts-pattern` akan memicu kesalahan waktu kompilasi pada panggilan `.exhaustive()`, memberi tahu kita bahwa pencocokan tidak lengkap.
Fitur Lanjutan `ts-pattern`
`ts-pattern` jauh melampaui pencocokan properti sederhana:
- Pencocokan Predikat dengan `.when()`: Cocokkan berdasarkan kondisi.
match(input) .when(isString, (str) => `It's a string: ${str}`) .when(isNumber, (num) => `It's a number: ${num}`) .otherwise(() => 'It is something else'); - Pola Bersarang Dalam: Cocokkan struktur objek yang kompleks.
match(user) .with({ address: { city: 'Paris' } }, () => 'User is in Paris') .otherwise(() => 'User is elsewhere'); - Wildcard dan Pemilih Khusus: Gunakan `P.select()` untuk menangkap nilai dalam pola, atau `P.string`, `P.number` untuk mencocokkan nilai apa pun dari tipe tertentu.
import { match, P } from 'ts-pattern'; match(event) .with({ type: 'USER_LOGIN', user: { name: P.select() } }, (name) => { console.log(`${name} logged in.`); }) .otherwise(() => {});
Dengan menggunakan pustaka seperti `ts-pattern`, Anda mendapatkan yang terbaik dari kedua dunia: keamanan waktu kompilasi yang kuat dari pemeriksaan `never` TypeScript, digabungkan dengan API yang bersih, deklaratif, dan sangat ekspresif.
Masa Depan: Proposal Pencocokan Pola TC39
Bahasa JavaScript itu sendiri sedang dalam perjalanan untuk mendapatkan pencocokan pola asli. Ada proposal aktif di TC39 (komite yang menstandarkan JavaScript) untuk menambahkan ekspresi `match` ke bahasa tersebut.
Sintaks yang Diusulkan
Sintaksnya kemungkinan akan terlihat seperti ini:
// Ini adalah sintaks JavaScript yang diusulkan dan dapat berubah
const getMessage = (response) => {
return match (response) {
when ({ status: 200, body: b }) { return `Success with body: ${b}`; }
when ({ status: 404 }) { return 'Not Found'; }
when ({ status: s if s >= 500 }) { return `Server Error: ${s}`; }
default { return 'Unknown response'; }
}
};
Bagaimana dengan Keamanan Tipe?
Ini adalah pertanyaan krusial untuk diskusi kita. Dengan sendirinya, fitur pencocokan pola JavaScript asli akan melakukan pemeriksaannya saat runtime. Ia tidak akan tahu tentang tipe TypeScript Anda.
Namun, hampir pasti bahwa tim TypeScript akan membangun analisis statis di atas sintaks baru ini. Sama seperti TypeScript menganalisis pernyataan `if` dan blok `switch` untuk melakukan penyempitan tipe, ia akan menganalisis ekspresi `match`. Ini berarti kita akhirnya bisa mendapatkan hasil terbaik:
- Sintaks Asli, Berkinerja: Tidak perlu pustaka atau trik transpilasi.
- Keamanan Waktu Kompilasi Penuh: TypeScript akan memeriksa ekspresi `match` untuk kelengkapan terhadap gabungan yang didiskriminasikan, sama seperti yang dilakukannya saat ini untuk `switch`.
Sementara kita menunggu fitur ini untuk melewati tahap proposal dan masuk ke peramban dan runtime, teknik yang telah kita diskusikan hari ini dengan gabungan yang didiskriminasikan dan pustaka adalah solusi yang siap produksi dan canggih.
Aplikasi Praktis dan Praktik Terbaik
Mari kita lihat bagaimana pola-pola ini berlaku untuk skenario pengembangan umum di dunia nyata.
Manajemen Status (Redux, Zustand, dll.)
Mengelola status dengan tindakan adalah kasus penggunaan yang sempurna untuk gabungan yang didiskriminasikan. Alih-alih menggunakan konstanta string untuk tipe tindakan, definisikan gabungan yang didiskriminasikan untuk semua tindakan yang mungkin.
// Tentukan tindakan
interface IncrementAction { type: 'counter/increment'; payload: number; }
interface DecrementAction { type: 'counter/decrement'; payload: number; }
interface ResetAction { type: 'counter/reset'; }
type CounterAction = IncrementAction | DecrementAction | ResetAction;
// Pengurang yang aman tipe
function counterReducer(state: number, action: CounterAction): number {
return match(action)
.with({ type: 'counter/increment' }, (act) => state + act.payload)
.with({ type: 'counter/decrement' }, (act) => state - act.payload)
.with({ type: 'counter/reset' }, () => 0)
.exhaustive();
}
Sekarang, jika Anda menambahkan tindakan baru ke gabungan `CounterAction`, TypeScript akan memaksa Anda untuk memperbarui pengurang. Tidak ada lagi handler tindakan yang terlupakan!
Menangani Respons API
Mengambil data dari API melibatkan beberapa status: memuat, berhasil, dan kesalahan. Memodelkan ini dengan gabungan yang didiskriminasikan membuat logika UI Anda jauh lebih tangguh.
// Model status data asinkron
// Dalam komponen UI Anda (misalnya, React)
function UserProfile({ userId }: { userId: string }) {
const [userState, setUserState] = useState>({ status: 'idle' });
// ... useEffect untuk mengambil data dan memperbarui status ...
return match(userState)
.with({ status: 'idle' }, () => Click a button to load the user.
)
.with({ status: 'loading' }, () => )
.with({ status: 'success' }, (state) => )
.with({ status: 'error' }, (state) => )
.exhaustive();
}
Pendekatan ini menjamin bahwa Anda telah menerapkan UI untuk setiap status pengambilan data Anda yang mungkin. Anda tidak dapat secara tidak sengaja lupa untuk menangani kasus memuat atau kesalahan.
Ringkasan Praktik Terbaik
- Model dengan Gabungan yang Didiskriminasikan: Kapan pun Anda memiliki nilai yang dapat berupa salah satu dari beberapa bentuk yang berbeda, gunakan gabungan yang didiskriminasikan. Ini adalah landasan pola yang aman tipe di TypeScript.
- Selalu Terapkan Kelengkapan: Baik Anda menggunakan trik `never` dengan pernyataan `switch` atau metode `.exhaustive()` pustaka, jangan pernah membiarkan pencocokan pola terbuka. Di sinilah keamanannya berasal.
- Pilih Alat yang Tepat: Untuk kasus sederhana, pernyataan `switch` sudah cukup. Untuk logika yang kompleks, pencocokan bersarang, atau gaya yang lebih fungsional, pustaka seperti `ts-pattern` akan secara signifikan meningkatkan keterbacaan dan mengurangi boilerplate.
- Jaga Agar Pola Tetap Mudah Dibaca: Tujuannya adalah kejelasan. Hindari pola bersarang yang terlalu kompleks yang sulit dipahami sekilas. Terkadang, memecah pencocokan menjadi fungsi yang lebih kecil adalah pendekatan yang lebih baik.
Kesimpulan: Menulis Masa Depan JavaScript yang Aman
Pencocokan pola lebih dari sekadar gula sintaksis; ini adalah paradigma yang mengarah pada kode yang lebih deklaratif, dapat dibaca, dan—yang terpenting—lebih tangguh. Sementara kita dengan antusias menunggu kedatangannya secara native di JavaScript, kita tidak perlu menunggu untuk menuai manfaatnya.
Dengan memanfaatkan kekuatan sistem tipe statis TypeScript, terutama dengan gabungan yang didiskriminasikan, kita dapat membangun sistem yang dapat diverifikasi saat waktu kompilasi. Pendekatan ini secara fundamental menggeser deteksi bug dari runtime ke waktu pengembangan, menghemat jam debugging yang tak terhitung jumlahnya dan mencegah insiden produksi. Pustaka seperti `ts-pattern` membangun fondasi yang kokoh ini, menyediakan API yang elegan dan kuat yang membuat penulisan kode yang aman tipe menjadi menyenangkan.
Menerapkan verifikasi pencocokan waktu kompilasi adalah langkah menuju penulisan aplikasi yang lebih tangguh dan dapat dipelihara. Ini mendorong Anda untuk secara eksplisit memikirkan semua kemungkinan status data Anda, menghilangkan ambiguitas dan membuat logika kode Anda sangat jelas. Mulailah memodelkan domain Anda dengan gabungan yang didiskriminasikan hari ini, dan biarkan kompiler TypeScript menjadi mitra tak kenal lelah Anda dalam membangun perangkat lunak bebas bug.