Optional Chaining pada JavaScript
ES2020 mengenalkan fitur Optional Chaining, yang sudah kita bahas kisi-kisinya pada Bekerja dengan Ketiadaan di JavaScript (Bagian 2). Kali ini, kita akan menyelami fitur baru ini lebih dalam.
Motivasi Optional Chaining
JavaScript akan memberikan TypeError
jika kita mencoba mengakses properti dari undefined
atau null
. Masuk akal memang, tapi jika kita bekerja dengan objek dengan struktur berlapis, sifat tersebut menjadi hal yang penting untuk diperhatikan.
let obj = {};
obj.a; // undefined
obj.a.b; // TypeError: Cannot read property 'b' of undefined
Dengan operator typeof
dan operator logika &&
, kita bisa memastikan apakah akses properti objek bisa dilakukan dengan aman.
let obj = {};
(typeof obj.a !== 'undefined' && obj.a !== null ) && obj.a.b; // false, tapi tidak error
Lebih jauh lagi, kita bisa menggunakan operator logika ||
untuk mengatur nilai fallback.
let obj = {};
(typeof obj.a !== 'undefined' && obj.a !== null ) && obj.a.b || "tidak ada nilai";
// "tidak ada nilai"
Tapi, tidakkah sintaks itu terlalu panjang? Sintaks yang panjang dengan menggunakan operator yang tidak dibuat untuk kasus pakai yang dilakukan akan membuka peluang terjadinya bug, dan mencederai keterbacaan kode. Optional Chaining dihadirkan untuk memecahkan masalah tersebut.
Penggunaan Optional Chaining
Sintaks
Optional Chaining pada JavaScript memiliki sintaks tanda tanya diikuti titik (?.
). Terdapat tiga tempat di mana sintaks Optional Chaining dapat digunakan:
- Akses properti objek secara statis (
a?.b
) - Akses properti objek secara dinamis (
a?.[expr]
) - Pemanggilan fungsi (
a?.(...args)
)
Cara Kerja Optional Chaining
Operator Optional Chaining membutuhkan dua ekspresi sebagai operan, di sebelah kiri (LHS) dan sebelah kanan (RHS) dari operan tersebut (misalnya lhs?.rhs
). Jika LHS bernilai nullish, maka operasi Optional Chaining akan langsung mengembalikan nilai undefined
dan ekspresi RHS tidak akan dievaluasi dan tidak akan mengembalikan error.
const obj = {};
obj.a?.b(); // undefined
obj.a?.[++x]; // undefined
obj.a?.b.c().d?.[e].f; // undefined
Sifat ini disebut short-circuiting. Dengan adanya sifat ini, sepanjang apapun operan RHS, ia tidak akan dievaluasi jika LHS bernilai nullish sehingga tidak akan menimbulkan error.
Caveat
Perlu diperhatikan sekali bahwa ketika LHS bernilai bukan nullish, maka RHS akan dievaluasi, sehingga Optional Chaining perlu dilakukan pada tempat-tempat di mana kita merasa mungkin akan terjadi nilai nullish, pada bagian RHS operator Optional Chaining pertama sekalipun.
const obj = { a: 42 };
obj.a?.length.toString(); // TypeError
Pada contoh di atas, obj.a
tidak bernilai nullish sehingga obj.a.length
dievaluasi bernilai undefined
, lalu terjadi TypeError
karena undefined
tidak memiliki properti toString
. Agar lebih aman, kita harus mengubahnya menjadi:
obj.a?.length?.toString();
Kenapa tidak dibuat agar satu operasi Optional Chaining mencakup keseluruhan ekspresi, jadi jika ada nilai nullish di RHS pun akan mengembalikan undefined
alih-alih TypeError
?
Desain tersebut dibuat secara sengaja agar pengembang dapat mengindikasikan percabangan pada kodenya secara eksplisit, karena pada dasarnya operator ini bekerja sebagai percabangan if-else. Kode jadi akan lebih mudah di-debug ketika terjadi kesalahan karena eksplisitasnya.
Contoh Kasus Optional Chaining
Akses Properti Statis
Misalnya Anda bekerja dengan API untuk mengambil data pengguna seperti di bawah ini, dan Anda ingin mengambil kode pos pengguna tersebut.
{
"name": "John Doe"
"address": {
"province": "DKI Jakarta",
"city": "Jakarta Selatan",
"zip_code": 12510
}
}
Tetapi, jika pengguna belum mengatur alamat, maka API akan mengembalikan nilai null
untuk atribut address
-nya. Anda bisa menggunakan Optional Chaining untuk mengakses zip_code
dengan aman.
const zipCode = response.address?.zip_code;
Akses Elemen Array dan Properti Dinamis
Masih dengan contoh yang sama, tapi kali ini pengguna memiliki atribut keranjang belanja cart
berupa array, dan Anda ingin mendapatkan barang pertama pada keranjang belanja tersebut.
{
"name": "John Doe"
"cart": [
{
"id": 1,
"name": "Magic Keyboard"
}
]
}
Tetapi, jika pengguna belum menambahkan barang ke keranjang sama sekali, maka nilai cart
akan menjadi null
. Anda bisa melakukannya dengan cara berikut.
const firstItem = response.cart?.[0];
Atau, katakanlah Anda ingin mengambil barang acak dari keranjang tersebut untuk diberi diskon dadakan, maka operator akses properti dinamis bisa digunakan.
const item = response.cart?.[
Math.round(Math.random() * (response.cart.length - 1))
];
Akses Fungsi atau Metode
Masih dengan API yang sama dengan contoh di atas, Anda tahu bahwa properti cart
bisa bertipe array atau bernilai null
. Anda ingin menjumlahkan harga seluruh barang dalam keranjang belanja pengguna. Anda bisa melakukannya dengan aman dengan bantuan Optional Chaining pada pemanggilan fungsi.
const total = response.cart?.reduce?.(
(subtotal, item) => subtotal + item.price,
0
);
Dengan begitu, jika cart
tidak memiliki metode reduce
, ekspresi ini akan mengembalikan undefined
.
Satu hal yang harus diperhatikan pada kasus pakai ini, operator Optional Chaining tetap konsisten dengan cara kerjanya. Ketika nama fungsi yang dipanggil tidak bernilai nullish, meskipun ia bukanlah sebuah fungsi, maka ia akan tetap dipanggil. Operator ini tidak memeriksa apakah atribut yang dipanggil adalah fungsi atau bukan. Risiko TypeError
tetap ada.
const obj = { a: 42 };
obj.a?.(); // TypeError: obj.a is not a function
Kolaborasi dengan Nullish Coalescing Operator
Tentu mengembalikan nilai undefined
bukan jawaban yang ideal untuk seluruh kasus penggunaan ekspresi ini. Kita perlu suatu cara untuk mengatur nilai kembalian fallback, yang disediakan oleh Nullish Coalescing Operator (??
).
Nullish Coalescing Operator akan mengevaluasi dan mengembalikan ekspresi di sebelah kanan jika operan sebelah kiri bernilai nullish.
const streetName = response.address?.street_name ?? "Belum mengatur alamat";
Sifat yang perlu diperhatikan dari operasi ini adalah ia juga short-circuiting, jika operan kiri bernilai nullish, ekspresi pada operan kanan tidak akan dievaluasi sama sekali.
const counter = 0;
const obj = { counter: 1 };
obj.counter++ ?? ++counter;
console.log(obj.counter); // 2
console.log(counter); // 0
Yang Tidak Dilakukan Optional Chaining Operator
Ada beberapa ekspresi Optional Chaining yang dipertimbangkan oleh tim TC39, tetapi diputuskan agar tidak didukung karena ketiadaan kasus penggunaan yang meyakinkan di dunia nyata, atau alasan-alasan lain. Berikut adalah beberapa contohnya.
- Konstruksi objek:
new a?.()
- Template Literal:
a?.`string`
- Konstruktor atau Template Literal dalam/setelah Optional Chain:
new a?.b()
,a?.b`string`
- Assignment:
a?.b = c
- Super:
super?.()
,super?.foo