Union membolehkan satu variabel menampung beberapa tipe, tetapi kode sering perlu mengetahui tipe yang sedang ditampung agar dapat mengakses properti yang tepat. Proses mempersempit tipe berdasarkan pemeriksaan saat runtime disebut narrowing. Topik ini membahas narrowing melalui typeof, operator in, instanceof, uji truthiness, serta pemeriksaan ekshaustif dengan tipe never.
Mempersempit dengan typeof#
Operator typeof pada JavaScript mengembalikan nama tipe sebuah nilai dalam bentuk string, misalnya "string" atau "number". Compiler TypeScript memahami pemeriksaan ini dan mempersempit union di dalam setiap percabangannya. Di dalam blok if yang menguji typeof nilai === "string", variabel nilai diperlakukan bertipe string, sedangkan pada cabang lain variabel yang sama bertipe sisa union.
function labelNilai(nilai: string | number): string {
if (typeof nilai === "string") {
return `${nilai.length} karakter`;
}
return `${nilai} poin`;
}
console.log(labelNilai("Matematika"));
console.log(labelNilai(88));10 karakter
88 poinPada contoh, pemanggilan nilai.length hanya sah di dalam cabang string, sedangkan pemakaian nilai langsung pada cabang kedua sah karena nilainya sudah terjamin bertipe number. Tanpa narrowing, salah satu akses properti akan ditolak compiler karena tidak semua anggota union memilikinya. Keluarannya menunjukkan kedua cabang menangani tipe masing-masing dengan benar.
Pemeriksaan typeof bekerja sama baik di dalam switch maupun dalam bentuk negasinya. Setelah if (typeof nilai !== "string"), variabel yang sama dijamin bukan string pada blok berikutnya, sehingga tipe variabel itu menyempit menjadi sisa union. Prinsip yang sama berlaku pada seluruh teknik narrowing di topik ini, bukan hanya pada typeof.
Operator in untuk properti pembeda#
Operator in menguji keberadaan sebuah properti pada objek, dan compiler menggunakannya untuk membedakan anggota union yang propertinya berbeda. Uji ini berguna ketika dua tipe tidak dapat dibedakan dengan typeof, misalnya dua tipe objek yang sama-sama dikenali oleh typeof sebagai objek.
type TiketReguler = { barisan: number };
type TiketVip = { barisan: number; nomorKursi: string };
function deskripsiTiket(tiket: TiketReguler | TiketVip): string {
if ("nomorKursi" in tiket) {
return `VIP kursi ${tiket.nomorKursi}`;
}
return `Reguler barisan ${tiket.barisan}`;
}
console.log(deskripsiTiket({ barisan: 5, nomorKursi: "A12" }));
console.log(deskripsiTiket({ barisan: 5 }));VIP kursi A12
Reguler barisan 5Di dalam cabang if, tiket dijamin bertipe TiketVip karena hanya tipe tersebut yang memiliki properti nomorKursi, sehingga akses ke properti itu sah. Pada cabang else, anggota yang tersisa hanya TiketReguler, sehingga pembacaan tiket.barisan tetap aman. Pembeda berupa keberadaan properti semacam ini sering disebut discriminator.
Uji in memeriksa keberadaan kunci, bukan kebenaran nilainya, sehingga properti bernilai 0 atau string kosong tetap lolos. Sifat itu membuat in aman dipakai pada nilai semacam itu, berbeda dari uji truthiness yang menguji kebenaran nilai secara langsung. Pembedaan dengan in juga tidak terbatas pada dua tipe, karena union dengan lebih banyak anggota tetap terbagi sesuai keberadaan kuncinya.
instanceof untuk pembedaan kelas#
Operator instanceof menguji apakah sebuah nilai dibuat dari kelas tertentu, dan compiler mempersempit union berdasarkan hasil ujinya. Uji ini cocok untuk nilai yang memang berupa instance kelas, misalnya dua jenis pesan yang dibangun dari dua kelas berbeda.
class PesanTeks {
isi: string;
constructor(isi: string) {
this.isi = isi;
}
}
class PesanGagal {
kode: number;
constructor(kode: number) {
this.kode = kode;
}
}
function ringkasan(pesan: PesanTeks | PesanGagal): string {
if (pesan instanceof PesanGagal) {
return `Gagal dengan kode ${pesan.kode}`;
}
return `Teks: ${pesan.isi}`;
}
console.log(ringkasan(new PesanTeks("sinkron selesai")));
console.log(ringkasan(new PesanGagal(404)));Teks: sinkron selesai
Gagal dengan kode 404Cabang instanceof PesanGagal menjamin pesan bertipe PesanGagal, sehingga properti kode dapat dibaca. Cabang berikutnya otomatis bertipe PesanTeks. Pemisahan ini berlaku untuk kelas mana pun, termasuk kelas bawaan seperti Date dan Error, karena yang diuji adalah konstruktornya. Pola yang sama membuat error instanceof Error lazim di dalam blok catch, karena nilai yang ditangkap tidak memiliki kelas yang pasti.
Uji truthiness#
Uji truthiness adalah pola if yang menguji nilai secara langsung tanpa operator perbandingan. JavaScript menganggap nilai false, 0, "", null, undefined, dan NaN sebagai falsy, sedangkan sisanya truthy. Compiler TypeScript memanfaatkan pemeriksaan tersebut untuk menyaring anggota union yang falsy, misalnya null dan undefined.
function sapa(nama: string | undefined): string {
if (nama !== undefined) {
return `Selamat datang, ${nama}.`;
}
return "Selamat datang, tamu.";
}
console.log(sapa("Citra"));
console.log(sapa(undefined));Selamat datang, Citra.
Selamat datang, tamu.Fungsi pada contoh sengaja memeriksa nama !== undefined, bukan if (nama). Alasannya, string kosong juga falsy, sehingga uji if (nama) memperlakukan nama kosong seolah tidak ada. Bagian Kesalahan umum membahas gejala tersebut secara langsung dengan keluaran nyata.
Uji truthiness paling sering digunakan untuk menyaring null dan undefined sebelum nilai dipakai. Pada union string | undefined, cabang if (nama) menjamin nama bertipe string, sedangkan cabang else tetap menampung string kosong yang juga falsy. Ketepatan pilihan di antara keduanya ditentukan oleh makna nilai kosong dalam program, bukan oleh kenyamanan penulisan.
Pemeriksaan ekshaustif dengan never#
Pemeriksaan ekshaustif memastikan seluruh anggota union ditangani oleh percabangan. Kuncinya adalah tipe never, karena variabel bertipe never tidak boleh diisi apa pun. Bila cabang default menugaskan nilai yang tersisa ke never, compiler menerima program selama tidak ada anggota yang terlewat.
type Bentuk =
| { jenis: "lingkaran"; radius: number }
| { jenis: "persegi"; sisi: number };
function luas(bentuk: Bentuk): number {
switch (bentuk.jenis) {
case "lingkaran":
return 3.14 * bentuk.radius * bentuk.radius;
case "persegi":
return bentuk.sisi * bentuk.sisi;
default:
const selesai: never = bentuk;
return selesai;
}
}
console.log(luas({ jenis: "lingkaran", radius: 2 }));
console.log(luas({ jenis: "persegi", sisi: 4 }));12.56
16// tidak-dijalankan
type Bentuk =
| { jenis: "lingkaran"; radius: number }
| { jenis: "persegi"; sisi: number }
| { jenis: "segitiga"; alas: number; tinggi: number };
function luas(bentuk: Bentuk): number {
switch (bentuk.jenis) {
case "lingkaran":
return 3.14 * bentuk.radius * bentuk.radius;
case "persegi":
return bentuk.sisi * bentuk.sisi;
default:
const selesai: never = bentuk;
return selesai;
}
}# varian segitiga belum ditangani, sehingga penugasan ke never gagal
tsc --noEmit ekshaustif-bocor.tsekshaustif-bocor.ts(14,13): error TS2322: Type '{ jenis: "segitiga"; alas: number; tinggi: number; }' is not assignable to type 'never'.Varian segitiga pada berkas kedua ditambahkan ke union tanpa menambah cabangnya. Penugasan ke never kini gagal, karena nilai yang tersisa di default berbentuk objek segitiga dan bukan lagi never. Error semacam ini muncul tepat pada baris penugasan, sehingga anggota union yang terlewat langsung diketahui.
Pemeriksaan yang sama bekerja dengan if dan else if, karena yang diuji adalah alur yang tersisa, bukan bentuk pernyataannya. Pemeriksaan ekshaustif juga tidak mensyaratkan objek dengan properti pembeda, karena union literal seperti "tunda" | "berjalan" dapat diperiksa langsung dari nilainya. Manfaat terbesarnya muncul ketika union berkembang, karena compiler menandai setiap percabangan yang tertinggal tanpa pencarian manual.
Kesalahan umum#
Kekeliruan yang paling sering adalah menguji keberadaan nilai dengan truthiness semata. Berkas berikut memperlihatkan gejalanya, yaitu string kosong yang diperlakukan sebagai ketiadaan.
function sapa(nama: string | undefined): string {
if (nama) {
return `Selamat datang, ${nama}.`;
}
return "Selamat datang, tamu.";
}
console.log(sapa("Citra"));
console.log(sapa(""));Selamat datang, Citra.
Selamat datang, tamu.Pemanggilan dengan string kosong jatuh ke sapaan tamu karena "" falsy, padahal nilainya benar-benar ada. Penyebabnya adalah pemakaian uji kebenaran pada nilai yang memperbolehkan 0 dan "" sebagai nilai sah, dan perbaikannya adalah membandingkan secara eksplisit, misalnya nama !== undefined atau nama.length > 0. Pola eksplisit membuat maksud kode selaras dengan perilakunya.
Kekeliruan kedua adalah mengharapkan narrowing menyeberang ke dalam callback. Pemeriksaan alur hanya berlaku pada cakupan yang sama, sehingga variabel union yang dipersempit di luar fungsi kembali bertipe union penuh saat dibaca di dalam callback seperti pada filter. Perbaikannya adalah menampung hasil narrowing ke konstanta baru, lalu konstanta itulah yang diteruskan ke callback.
Ringkasan#
typeof, operatorin,instanceof, dan uji truthiness mempersempit union berdasarkan pemeriksaan yang dipahami compiler.- Pemeriksaan ekshaustif memanfaatkan penugasan ke
neverpada cabang default agar anggota union yang terlewat dilaporkan compiler. - Uji truthiness menyaring nilai falsy termasuk
""dan0, sehingga perbandingan eksplisit lebih aman untuk nilai yang boleh falsy. - Narrowing hanya berlaku pada cakupan yang sama dan tidak menyeberang ke dalam callback.
Referensi#
- Narrowing — The TypeScript Handbook — Sumber utama pola typeof, in, instanceof, truthiness, dan exhaustiveness checking.