Seleksi Kondisi #
Seleksi kondisi adalah mekanisme yang menentukan jalur eksekusi kode berdasarkan suatu kondisi — fondasi dari hampir semua logika program. TypeScript mewarisi seluruh konstruksi kondisional dari JavaScript (if-else, switch, ternary), tapi menambahkan dimensi yang sangat penting: type narrowing. Setiap kali kamu menulis kondisi di TypeScript, compiler secara otomatis menyempitkan tipe variabel di dalam blok kondisi berdasarkan konteks kondisi tersebut. Memahami bagaimana narrowing bekerja — dan bagaimana menulis type guard yang efektif — adalah keterampilan kunci yang membedakan TypeScript yang ditulis biasa dari TypeScript yang benar-benar memanfaatkan sistem tipenya secara penuh.
if-else dan Type Narrowing Otomatis
#
if-else adalah konstruksi kondisional paling mendasar. Di TypeScript, setiap ekspresi kondisi yang kamu tulis memberikan informasi tipe kepada compiler — ia “mempersempit” tipe variabel berdasarkan apa yang sudah dipastikan oleh kondisi tersebut.
function prosesInput(nilai: string | number | null): string {
// Di sini: nilai bertipe string | number | null
if (nilai === null) {
// Di sini: TypeScript tahu nilai pasti null
return "Tidak ada nilai";
}
// Di sini: TypeScript tahu nilai pasti string | number (null sudah dikecualikan)
if (typeof nilai === "string") {
// Di sini: TypeScript tahu nilai pasti string
return nilai.toUpperCase(); // ✓ .toUpperCase() tersedia
}
// Di sini: TypeScript tahu nilai pasti number
return nilai.toFixed(2); // ✓ .toFixed() tersedia
}
Narrowing terjadi secara otomatis berdasarkan berbagai jenis pemeriksaan yang TypeScript pahami:
type Status = "aktif" | "nonaktif" | "pending" | null | undefined;
function deskripsikanStatus(status: Status): string {
// Narrowing dengan kesetaraan
if (status === "aktif") {
return "Pengguna sedang aktif"; // status: "aktif"
}
if (status === null || status === undefined) {
return "Status tidak diketahui"; // status: null | undefined
}
// Di sini: status pasti "nonaktif" | "pending"
if (status === "nonaktif") {
return "Pengguna tidak aktif"; // status: "nonaktif"
}
// Di sini: TypeScript tahu status pasti "pending"
return "Menunggu konfirmasi"; // status: "pending"
}
Pola Early Return — Mengurangi Nesting #
Salah satu teknik terpenting dalam menulis kondisi yang bersih adalah early return: kembalikan nilai sesegera mungkin untuk kondisi tepi, lalu tangani kasus utama tanpa nesting berlebihan.
// ANTI-PATTERN: Kondisi bertingkat yang dalam — sulit dibaca
function hitungBonus(
karyawan: { aktif: boolean; nilai: number; masa: number } | null
): number {
if (karyawan !== null) {
if (karyawan.aktif) {
if (karyawan.nilai >= 80) {
if (karyawan.masa >= 2) {
return karyawan.nilai * 500_000;
} else {
return karyawan.nilai * 250_000;
}
} else {
return 0;
}
} else {
return 0;
}
} else {
return 0;
}
}
// BENAR: Early return — flat, mudah dibaca, mudah diuji
function hitungBonus2(
karyawan: { aktif: boolean; nilai: number; masa: number } | null
): number {
if (karyawan === null) return 0; // Guard: data tidak ada
if (!karyawan.aktif) return 0; // Guard: karyawan tidak aktif
if (karyawan.nilai < 80) return 0; // Guard: nilai tidak memenuhi syarat
// Kasus utama — semua guard sudah terlewati
const multiplier = karyawan.masa >= 2 ? 500_000 : 250_000;
return karyawan.nilai * multiplier;
}
switch-case dan Exhaustive Check
#
switch-case ideal ketika kamu perlu memeriksa satu variabel terhadap banyak nilai diskret. Di TypeScript, switch juga melakukan narrowing — di dalam setiap case, TypeScript tahu nilai variabel persis sama dengan nilai case tersebut.
type MetodePembayaran = "transfer" | "kartu_kredit" | "gopay" | "ovo";
interface InstruksiPembayaran {
langkah: string[];
batasWaktu: number; // dalam menit
}
function ambilInstruksi(metode: MetodePembayaran): InstruksiPembayaran {
switch (metode) {
case "transfer":
return {
langkah: ["Buka aplikasi bank", "Transfer ke rekening tujuan", "Konfirmasi"],
batasWaktu: 60,
};
case "kartu_kredit":
return {
langkah: ["Masukkan nomor kartu", "Isi CVV dan tanggal kedaluwarsa", "Konfirmasi OTP"],
batasWaktu: 10,
};
case "gopay":
case "ovo": // Fall-through — dua case berbagi logika yang sama
return {
langkah: ["Buka aplikasi dompet digital", "Scan QR Code", "Konfirmasi pembayaran"],
batasWaktu: 5,
};
}
// TypeScript tahu switch sudah exhaustive — tidak perlu default jika semua case ditangani
}
Exhaustive Check dengan never
#
Pola paling powerful dari switch di TypeScript adalah memastikan semua kemungkinan union sudah ditangani. Jika kamu menambahkan nilai baru ke union tapi lupa menanganinya di switch, compiler akan melaporkan error:
type JenisNotifikasi = "email" | "sms" | "push";
function kirimNotifikasi(jenis: JenisNotifikasi, pesan: string): void {
switch (jenis) {
case "email":
console.log(`Kirim email: ${pesan}`);
break;
case "sms":
console.log(`Kirim SMS: ${pesan}`);
break;
case "push":
console.log(`Kirim push notification: ${pesan}`);
break;
default: {
// Jika semua case sudah ditangani, TypeScript tahu jenis bertipe never di sini
// Kalau ada tipe baru di union yang belum ditangani, baris ini akan error
const _tidakMungkin: never = jenis;
throw new Error(`Jenis notifikasi tidak dikenal: ${jenis}`);
}
}
}
// Sekarang tambahkan "whatsapp" ke union tanpa update switch
// type JenisNotifikasi = "email" | "sms" | "push" | "whatsapp";
// → TypeScript akan error di default case: Type '"whatsapp"' is not assignable to type 'never'
// Ini memaksamu menambahkan case "whatsapp" — tidak ada yang terlewat!
switch vs if-else — Kapan Memilih Mana
#
flowchart TD
A{Berapa nilai yang\ndibandingkan?} --> B[1-2 nilai]
A --> C[3+ nilai diskret]
A --> D[Rentang / kondisi\nnon-kesetaraan]
B --> E[if-else\nlebih ringkas]
C --> F{Tipe variabel\nadalah union literal?}
D --> G[if-else dengan\n>= <= > <]
F -- Ya --> H[switch-case\ndengan exhaustive check]
F -- Tidak --> I[if-else atau\nlookup object]
style E fill:#51cf66,color:#fff
style H fill:#339af0,color:#fff
style G fill:#51cf66,color:#fff
style I fill:#fcc419,color:#000Type Guard — Mempersempit Tipe Secara Kustom #
Type guard adalah fungsi atau ekspresi yang memberitahu TypeScript bagaimana menyempitkan tipe dalam konteks kondisional. TypeScript memahami beberapa bentuk type guard secara otomatis, tapi kamu juga bisa menulis type guard kustom.
Type Guard Bawaan #
TypeScript memahami narrowing dari beberapa pola ekspresi secara otomatis:
function prosesData(data: string | number | boolean | null | undefined): string {
// typeof guard — untuk tipe primitif
if (typeof data === "string") {
return data.trim(); // data: string
}
if (typeof data === "number") {
return data.toLocaleString("id-ID"); // data: number
}
// Kesetaraan — untuk null/undefined dan literal
if (data === null || data === undefined) {
return "Tidak ada data"; // data: null | undefined
}
// Setelah semua guard di atas, TypeScript tahu data: boolean
return data ? "Ya" : "Tidak";
}
Type Guard Kustom dengan is
#
Untuk union type yang melibatkan object, typeof tidak cukup — kamu perlu menulis fungsi type guard kustom dengan return type menggunakan sintaks nilai is Tipe:
interface Pengguna {
tipe: "pengguna";
nama: string;
email: string;
}
interface Admin {
tipe: "admin";
nama: string;
levelAkses: number;
}
type Akun = Pengguna | Admin;
// Type guard kustom — return type "nilai is Admin" adalah kuncinya
function isAdmin(akun: Akun): akun is Admin {
return akun.tipe === "admin";
}
function tampilkanDasbor(akun: Akun): void {
if (isAdmin(akun)) {
// Di sini: TypeScript tahu akun bertipe Admin
console.log(`Admin level ${akun.levelAkses}: ${akun.nama}`);
// akun.email; // ✗ Error — Admin tidak punya properti email
} else {
// Di sini: TypeScript tahu akun bertipe Pengguna
console.log(`Pengguna: ${akun.nama} (${akun.email})`);
// akun.levelAkses; // ✗ Error — Pengguna tidak punya levelAkses
}
}
Type Guard untuk Validasi Data Eksternal #
Type guard sangat berguna saat memvalidasi data yang datang dari luar sistem (API response, input pengguna, JSON parsing):
interface ResponseAPI {
id: number;
nama: string;
email: string;
}
// Type guard yang memvalidasi bentuk object dari sumber eksternal
function isResponseAPI(data: unknown): data is ResponseAPI {
return (
typeof data === "object" &&
data !== null &&
"id" in data &&
"nama" in data &&
"email" in data &&
typeof (data as ResponseAPI).id === "number" &&
typeof (data as ResponseAPI).nama === "string" &&
typeof (data as ResponseAPI).email === "string"
);
}
async function ambilPengguna(id: number): Promise<ResponseAPI | null> {
const response = await fetch(`/api/pengguna/${id}`);
const data: unknown = await response.json();
if (isResponseAPI(data)) {
// TypeScript tahu data bertipe ResponseAPI di sini
return data;
}
console.error("Format response tidak valid:", data);
return null;
}
Assertion Function — Type Guard yang Melempar Error #
Assertion function adalah varian type guard yang melempar error jika kondisi tidak terpenuhi — alih-alih mengembalikan boolean:
// Fungsi assertion — tidak mengembalikan nilai, melainkan melempar jika gagal
function pastikan<T>(
nilai: T | null | undefined,
pesan: string
): asserts nilai is T {
if (nilai === null || nilai === undefined) {
throw new Error(`Assertion gagal: ${pesan}`);
}
}
function prosesKonfigurasi(env: NodeJS.ProcessEnv): void {
const databaseUrl = env.DATABASE_URL;
const secretKey = env.SECRET_KEY;
// Setelah pastikan(), TypeScript tahu nilai tidak null/undefined
pastikan(databaseUrl, "DATABASE_URL harus diset di environment");
pastikan(secretKey, "SECRET_KEY harus diset di environment");
// Di sini: databaseUrl dan secretKey bertipe string (bukan string | undefined)
console.log(`Koneksi ke: ${databaseUrl}`);
}
Discriminated Union — Kondisi Berbasis Tipe #
Discriminated union adalah pola di mana setiap anggota union memiliki properti literal yang unik sebagai “discriminant” — kunci yang membedakan satu anggota dari yang lain. Ini memungkinkan narrowing yang sangat bersih tanpa type guard kustom.
// Setiap state memiliki properti "status" sebagai discriminant
type StatePermintaan =
| { status: "idle" }
| { status: "memuat" }
| { status: "berhasil"; data: string[]; total: number }
| { status: "gagal"; kodeError: number; pesan: string };
function renderKonten(state: StatePermintaan): string {
switch (state.status) {
case "idle":
return "Tekan tombol untuk memuat data";
case "memuat":
return "Sedang memuat...";
case "berhasil":
// TypeScript tahu state.data dan state.total ada di sini
return `${state.total} item ditemukan: ${state.data.join(", ")}`;
case "gagal":
// TypeScript tahu state.kodeError dan state.pesan ada di sini
return `Error ${state.kodeError}: ${state.pesan}`;
}
}
// Penggunaan
const stateAwal: StatePermintaan = { status: "idle" };
const stateMemuat: StatePermintaan = { status: "memuat" };
const stateBerhasil: StatePermintaan = {
status: "berhasil",
data: ["Produk A", "Produk B"],
total: 2,
};
console.log(renderKonten(stateBerhasil));
// "2 item ditemukan: Produk A, Produk B"
Lookup Object sebagai Alternatif switch
#
Untuk kondisi sederhana yang hanya memetakan satu nilai ke nilai lain, lookup object adalah alternatif yang lebih ringkas dan efisien dari switch-case:
type KodeHTTP = 200 | 201 | 400 | 401 | 403 | 404 | 500;
// ANTI-PATTERN: switch-case untuk pemetaan nilai sederhana — verbose
function pesanHTTPSwitch(kode: KodeHTTP): string {
switch (kode) {
case 200: return "OK";
case 201: return "Created";
case 400: return "Bad Request";
case 401: return "Unauthorized";
case 403: return "Forbidden";
case 404: return "Not Found";
case 500: return "Internal Server Error";
}
}
// BENAR: Lookup object — ringkas dan mudah diperluas
const PESAN_HTTP: Record<KodeHTTP, string> = {
200: "OK",
201: "Created",
400: "Bad Request",
401: "Unauthorized",
403: "Forbidden",
404: "Not Found",
500: "Internal Server Error",
};
function pesanHTTP(kode: KodeHTTP): string {
return PESAN_HTTP[kode];
}
// Lookup object juga bisa menyimpan fungsi (strategy pattern)
type FormatTanggal = "pendek" | "panjang" | "iso";
const formatter: Record<FormatTanggal, (d: Date) => string> = {
pendek: (d) => d.toLocaleDateString("id-ID"),
panjang: (d) => d.toLocaleDateString("id-ID", { weekday: "long", year: "numeric", month: "long", day: "numeric" }),
iso: (d) => d.toISOString(),
};
function formatTanggal(tanggal: Date, format: FormatTanggal): string {
return formatter[format](tanggal);
}
console.log(formatTanggal(new Date(), "panjang"));
// "Kamis, 7 Mei 2026"
Kondisi dengan Optional Chaining dan Nullish Coalescing #
Kombinasi ?. dan ?? memungkinkan penulisan kondisi untuk nilai nullable yang jauh lebih ringkas dari if-else manual:
interface Artikel {
judul: string;
penulis?: {
nama: string;
verifikasi?: boolean;
};
kategori?: string[];
}
const artikel: Artikel = {
judul: "Belajar TypeScript",
};
// ANTI-PATTERN: if-else bertingkat untuk nilai nullable
let namaPenulis: string;
if (artikel.penulis !== undefined) {
namaPenulis = artikel.penulis.nama;
} else {
namaPenulis = "Anonim";
}
let terverifikasi: boolean;
if (artikel.penulis !== undefined && artikel.penulis.verifikasi !== undefined) {
terverifikasi = artikel.penulis.verifikasi;
} else {
terverifikasi = false;
}
// BENAR: Optional chaining + nullish coalescing — ringkas dan ekspresif
const namaPenulis2 = artikel.penulis?.nama ?? "Anonim";
const terverifikasi2 = artikel.penulis?.verifikasi ?? false;
const kategoriUtama = artikel.kategori?.[0] ?? "Umum";
console.log(`${namaPenulis2} (${terverifikasi2 ? "✓" : "belum terverifikasi"})`);
// "Anonim (belum terverifikasi)"
Alur Memilih Konstruksi Kondisional #
flowchart TD
A{Apa yang ingin\nkamu lakukan?} --> B[Cek satu kondisi\nboolean]
A --> C[Bandingkan variabel\nke beberapa nilai]
A --> D[Petakan satu nilai\nke nilai lain]
A --> E[Cek tipe dari\nunion type]
A --> F[Akses properti\nnullable]
B --> B1[if-else]
C --> C1{Banyak kasus?}
C1 -- Ya, union literal --> C2[switch-case +\nexhaustive check]
C1 -- Tidak atau rentang --> C3[if-else if]
D --> D1[Lookup object\nRecord< K, V >]
E --> E1{Tipe primitif\natau object?}
E1 -- Primitif --> E2[typeof guard]
E1 -- Object/class --> E3[instanceof atau\ntype guard kustom]
F --> F1[Optional chaining ?.\ nNullish coalescing ??]
style B1 fill:#51cf66,color:#fff
style C2 fill:#339af0,color:#fff
style C3 fill:#51cf66,color:#fff
style D1 fill:#fcc419,color:#000
style E2 fill:#51cf66,color:#fff
style E3 fill:#339af0,color:#fff
style F1 fill:#51cf66,color:#fffRingkasan #
- Type narrowing otomatis — setiap konstruksi kondisional di TypeScript (
if,switch,typeof,instanceof,in,===) menyempitkan tipe variabel secara otomatis di dalam blok yang relevan; manfaatkan ini untuk menghindari type assertion manual.- Pola early return — kembalikan nilai sesegera mungkin untuk kondisi tepi (null check, validasi), lalu tangani kasus utama tanpa nesting berlebihan; ini meningkatkan keterbacaan dan mengurangi kompleksitas kognitif.
- Exhaustive check dengan
neverdidefaultcase switch — jika kamu menambahkan nilai baru ke union literal tapi lupa menanganinya di switch, compiler akan segera melaporkan error; ini adalah safety net yang sangat berharga.- Type guard kustom (
nilai is Tipe) — tulis fungsi type guard eksplisit untuk union type yang melibatkan object; ini membuat narrowing tersedia di seluruh kode, bukan hanya di satu titik.- Assertion function (
asserts nilai is T) — type guard yang melempar error alih-alih mengembalikan boolean; ideal untuk validasi prasyarat di awal fungsi.- Discriminated union adalah alternatif kondisional yang elegan — dengan properti discriminant literal, TypeScript otomatis mengetahui properti mana yang tersedia di setiap cabang tanpa type guard kustom.
- Lookup object lebih ringkas dari switch untuk pemetaan nilai sederhana — gunakan
Record<K, V>untuk pemetaan nilai ke nilai, atauRecord<K, () => V>untuk pemetaan nilai ke fungsi (strategy pattern).?.dan??menggantikan if-else untuk nilai nullable — optional chaining untuk akses aman, nullish coalescing untuk fallback; keduanya jauh lebih ringkas dan lebih sedikit rentan terhadap kesalahan ketik.