Type Narrowing — TypeScript

Type narrowing adalah proses mempersempit tipe berdasarkan kondisi: typeof guard: function proses(input: string | number) { if (typeof input === "string") {…

Type narrowing adalah cara TypeScript mempersempit type sebuah variabel di dalam satu cabang kode, berdasarkan pengecekan yang kamu tulis. Mirip menerima paket yang isinya "buku atau baju". Sebelum dibuka, kamu gak bisa langsung menaruhnya di rak buku. Begitu dibuka dan ternyata buku, kamu tahu pasti isinya buku. Kalau ternyata bukan buku, sisanya pasti baju.

🧪 Tebakan cepat: typeof ["a", "b"] hasilnya apa: "array", "object", atau "string"? Tebak dulu, jawabannya ada di bagian "Array: pakai Array.isArray".

typeof: cek type dasar

Di pelajaran union, kamu sudah memakai typeof sekali. Sekarang kita lihat apa yang TypeScript lakukan di tiap baris:

function panjang(input: string | string[]): number {
  if (typeof input === "string") {
    return input.length; // input: string
  }
  return input.length;   // input: string[]
}

console.log(panjang("hello"));         // 5
console.log(panjang(["a", "b", "c"])); // 3

Kalau kursor diarahkan ke input di editor, type yang muncul berbeda di tiap tempat:

Di mana Type input Kenapa
awal fungsi string | string[] belum ada pengecekan
di dalam if string typeof input === "string" bernilai true
setelah if string[] cabang string sudah return, jadi yang sampai ke sini pasti array

Array: pakai Array.isArray

typeof ["a", "b"] hasilnya "object", bukan "array". Itu jawaban tebakannya. Di JavaScript, array termasuk object, jadi typeof gak bisa membedakan array dari object lain. Untuk array, pakai Array.isArray:

function panjang(input: string | string[]): number {
  if (Array.isArray(input)) {
    return input.length; // input: string[]
  }
  return input.length;   // input: string
}

TypeScript juga paham Array.isArray: di dalam if, input jadi string[], dan setelahnya tinggal string.

in dan instanceof: cek object

Kalau anggota union-nya object, pakai in atau instanceof.

"nama" in objek mengecek apakah objek punya properti dengan nama itu:

type Ikan = { berenang: () => string };
type Kucing = { meong: () => string };

function gerak(hewan: Ikan | Kucing) {
  if ("berenang" in hewan) {
    return hewan.berenang(); // hewan: Ikan
  }
  return hewan.meong();      // hewan: Kucing
}

nilai instanceof Kelas mengecek apakah nilai itu dibuat dari sebuah class, misalnya Date:

function tahun(tanggal: string | Date) {
  if (tanggal instanceof Date) {
    return tanggal.getFullYear(); // tanggal: Date
  }
  return tanggal;                 // tanggal: string
}

Discriminated union: cek satu properti pembeda

Cara yang paling sering dipakai di aplikasi sungguhan: tiap anggota union punya properti yang sama, jenis, dengan isi literal yang beda. Properti ini namanya pembeda (discriminant).

type Lingkaran = { jenis: "lingkaran"; jari: number };
type Persegi = { jenis: "persegi"; sisi: number };
type Bentuk = Lingkaran | Persegi;

function luas(b: Bentuk): number {
  if (b.jenis === "lingkaran") {
    return Math.PI * b.jari * b.jari; // b: Lingkaran
  }
  if (b.jenis === "persegi") {
    return b.sisi * b.sisi;           // b: Persegi
  }
  const cek: never = b;               // b: never
  return cek;
}
Baris Type b
awal fungsi Bentuk
di dalam if (b.jenis === "lingkaran") Lingkaran
di dalam if (b.jenis === "persegi") Persegi
setelah dua if never

Begitu b.jenis sama dengan "lingkaran", cuma Lingkaran yang cocok, jadi b.jari boleh dipakai. b.sisi di cabang yang sama ditolak, karena lingkaran gak punya sisi.

Cek lengkap dengan never

never adalah type yang gak punya nilai sama sekali. Setelah dua if tadi, semua jenis sudah ditangani, jadi b jadi never dan const cek: never = b; lolos.

Sekarang Segitiga ditambahkan ke union, tapi fungsi luas lupa diubah:

type Segitiga = { jenis: "segitiga"; alas: number; tinggi: number };
type Bentuk = Lingkaran | Persegi | Segitiga;

Baris const cek: never = b; langsung error: Type 'Segitiga' is not assignable to type 'never'. (ts(2322)). TypeScript menunjuk jenis yang belum kamu tangani, sebelum kodenya sempat jalan. Itu gunanya baris never: alarm untuk cabang yang terlupa.

⚠️ Jebakan yang sering ketemu

TL;DR: Type narrowing adalah TypeScript mempersempit type di dalam sebuah cabang dari pengecekan yang kamu tulis. typeof x === "string" untuk type dasar, Array.isArray(x) untuk array (karena typeof array itu "object"), "properti" in x untuk object dengan properti berbeda, x instanceof Date untuk nilai dari class, dan x.jenis === "..." untuk discriminated union. Setelah cabang yang return, sisanya otomatis jadi type yang tersisa. Kalau semua jenis sudah ditangani, sisanya never, dan const cek: never = x; jadi alarm untuk jenis yang terlupa.

Yang akan kamu pelajari