Regex #
Regular expression (regex) adalah bahasa mini untuk mendeskripsikan pola dalam teks — salah satu alat yang paling powerful sekaligus paling mudah disalahgunakan dalam pemrograman. TypeScript mewarisi regex dari JavaScript secara penuh, tapi menambahkan type safety pada semua metode yang bekerja dengan regex: return type match(), exec(), replace(), dan sejenisnya semuanya terdefinisi dengan tepat, sehingga compiler bisa menangkap kesalahan seperti mengakses grup tangkapan yang tidak ada. Memahami regex di TypeScript berarti memahami tidak hanya sintaks pola, tapi juga bagaimana setiap metode berinteraksi dengan flag g, perbedaan antara RegExpMatchArray dan RegExpExecArray, serta jebakan lastIndex yang sering menyebabkan bug halus.
Membuat Regex — Literal vs Constructor #
Ada dua cara membuat regex di TypeScript:
// 1. Literal regex — pola dikompilasi saat parse time
const polaLiteral = /\d+/;
const polaLiteralFlag = /hello/gi;
// 2. RegExp constructor — pola dikompilasi saat runtime
// Berguna saat pola berasal dari input dinamis (variabel, database, config)
const polaDinamis = new RegExp("\\d+"); // \\ karena dalam string
const polaFlag = new RegExp("hello", "gi"); // Flag sebagai argumen kedua
// Kapan gunakan constructor: pola dari variabel
function buatValidatorPola(pola: string, flag = ""): RegExp {
return new RegExp(pola, flag);
}
const validatorKustom = buatValidatorPola("^\\+62\\d{9,12}$");
// PERHATIAN: Dalam string RegExp, backslash harus di-escape ganda
// /\d/ dalam literal = new RegExp("\\d") dalam constructor
// Ini sering menjadi sumber bug saat konversi antar keduanya
Metode Regex — Lengkap dengan Tipe Kembalian #
TypeScript mendefinisikan tipe kembalian yang presisi untuk semua metode regex. Memahami tipe kembalian ini penting untuk menghindari runtime error:
RegExp.prototype.test() — Pengecekan Boolean
#
const regexAngka = /^\d+$/;
// test() selalu mengembalikan boolean — paling sederhana dan aman
console.log(regexAngka.test("12345")); // true
console.log(regexAngka.test("abc")); // false
console.log(regexAngka.test("12a34")); // false
// Gunakan test() untuk validasi — tidak perlu hasil pencocokan
function isAngkaValid(input: string): boolean {
return /^\d{1,10}$/.test(input);
}
RegExp.prototype.exec() — Pencocokan dengan Detail
#
const regexTanggal = /(\d{4})-(\d{2})-(\d{2})/;
const teks = "Tanggal lahir: 1995-08-17";
const hasil = regexTanggal.exec(teks);
// Tipe: RegExpExecArray | null
if (hasil !== null) {
console.log(hasil[0]); // "1995-08-17" — kecocokan penuh
console.log(hasil[1]); // "1995" — grup 1
console.log(hasil[2]); // "08" — grup 2
console.log(hasil[3]); // "17" — grup 3
console.log(hasil.index); // 15 — posisi kecocokan dalam string
console.log(hasil.input); // Teks asli yang dicari
}
String.prototype.match() — Bergantung pada Flag g
#
Return type match() berbeda tergantung apakah regex menggunakan flag g atau tidak — ini adalah salah satu perilaku yang paling sering mengejutkan:
const teks = "Harga: 1000 dan 2500 dan 500";
// TANPA flag g — mengembalikan RegExpMatchArray | null (sama seperti exec)
const tanpaG = teks.match(/\d+/);
// Tipe: RegExpMatchArray | null
if (tanpaG) {
console.log(tanpaG[0]); // "1000" — hanya kecocokan pertama
console.log(tanpaG.index); // 7 — posisi tersedia
}
// DENGAN flag g — mengembalikan string[] | null (semua kecocokan, tanpa detail)
const denganG = teks.match(/\d+/g);
// Tipe: string[] | null — index dan grup tidak tersedia!
if (denganG) {
console.log(denganG); // ["1000", "2500", "500"] — semua kecocokan
// denganG.index; // ✗ Property 'index' tidak ada di string[]
}
String.prototype.matchAll() — Semua Kecocokan dengan Detail (ES2020)
#
matchAll() menggabungkan kelebihan keduanya — semua kecocokan dengan detail grup:
const logEntri = `
[2025-05-07] ERROR: Koneksi gagal
[2025-05-08] INFO: Server dimulai
[2025-05-09] WARN: Memori rendah
`;
// Regex HARUS menggunakan flag g untuk matchAll
const regexLog = /\[(\d{4}-\d{2}-\d{2})\] (\w+): (.+)/g;
// matchAll() mengembalikan IterableIterator<RegExpMatchArray>
for (const cocok of logEntri.matchAll(regexLog)) {
const [, tanggal, level, pesan] = cocok;
// Semua grup tersedia, plus cocok.index
console.log(`[${tanggal}] ${level}: ${pesan.trim()}`);
}
// Atau konversi ke array
const semuaCocok = [...logEntri.matchAll(regexLog)];
const entri = semuaCocok.map(([, tanggal, level, pesan]) => ({
tanggal,
level,
pesan: pesan.trim(),
}));
// Tipe: Array<{ tanggal: string; level: string; pesan: string }>
String.prototype.replace() dan replaceAll()
#
const teks = "harga: 1000, diskon: 200, total: 800";
// replace() dengan string — hanya mengganti kecocokan pertama
const ganti1 = teks.replace(/\d+/, "XXX");
// "harga: XXX, diskon: 200, total: 800"
// replace() dengan flag g — ganti semua kecocokan
const gantiSemua = teks.replace(/\d+/g, "XXX");
// "harga: XXX, diskon: XXX, total: XXX"
// replace() dengan fungsi — transformasi dinamis
const formatRupiah = teks.replace(/\d+/g, (angka) => {
return `Rp ${parseInt(angka).toLocaleString("id-ID")}`;
});
// "harga: Rp 1.000, diskon: Rp 200, total: Rp 800"
// replace() dengan referensi grup — $1, $2, dst
const formatTanggal = "2025-05-07".replace(/(\d{4})-(\d{2})-(\d{2})/, "$3/$2/$1");
// "07/05/2025"
// replaceAll() (ES2021) — tanpa perlu flag g
const gantiSemua2 = teks.replaceAll("000", "K");
// "harga: 1K, diskon: 200, total: 800"
String.prototype.split() dengan Regex
#
// split() bisa menggunakan regex sebagai delimiter
const kalimat = "Halo dunia, TypeScript itu keren";
// Pisahkan berdasarkan satu atau lebih spasi/koma/titik
const kata = kalimat.split(/[\s,]+/);
// ["Halo", "dunia", "TypeScript", "itu", "keren"]
// Pisahkan tapi pertahankan delimiter (gunakan capturing group)
const bagian = "satu1dua2tiga".split(/(\d)/);
// ["satu", "1", "dua", "2", "tiga"] — angka tetap ada sebagai elemen
Semua Flag Regex #
| Flag | Nama | Fungsi |
|---|---|---|
g | global | Cari semua kecocokan, bukan hanya pertama |
i | case-insensitive | Abaikan besar-kecil huruf |
m | multiline | ^ dan $ cocok di awal/akhir setiap baris |
s | dotAll | . cocok dengan karakter newline juga |
u | unicode | Aktifkan mode Unicode penuh (termasuk emoji) |
v | unicodeSets | Mode Unicode yang lebih lengkap (ES2024) |
d | indices | Sertakan informasi indeks untuk setiap grup |
y | sticky | Cocokkan hanya dari posisi lastIndex |
// Flag g — global: cari semua kecocokan
const semuaAngka = "abc123def456".match(/\d+/g);
// ["123", "456"]
// Flag i — case-insensitive
/hello/i.test("HELLO WORLD"); // true
// Flag m — multiline: ^ dan $ cocok per baris
const teksMultibaris = "baris pertama\nbaris kedua";
const cocokPerBaris = teksMultibaris.match(/^baris/gm);
// ["baris", "baris"] — keduanya cocok
// Flag s — dotAll: titik cocok dengan newline
const teksNewline = "awal\nakhir";
/awal.akhir/.test(teksNewline); // false — titik tidak cocok newline secara default
/awal.akhir/s.test(teksNewline); // true — dengan flag s
// Flag u — unicode: perlu untuk emoji dan karakter non-BMP
/^\p{Emoji}$/u.test("😊"); // true — cek apakah emoji (perlu flag u dan \p{})
// Flag d — indices: sertakan posisi awal-akhir tiap grup (ES2022)
const regexD = /(\d+)/d;
const hasilD = regexD.exec("abc123");
if (hasilD?.indices) {
console.log(hasilD.indices[0]); // [3, 6] — posisi kecocokan penuh
console.log(hasilD.indices[1]); // [3, 6] — posisi grup 1
}
Karakter Khusus dan Quantifier #
Karakter Khusus (Metacharacter) #
// Metacharacter — karakter dengan makna khusus dalam regex
// . \d \w \s \b \D \W \S \B ^ $ [] () {} | ? * +
// \d = digit (0-9)
/\d/.test("5"); // true
/\d/.test("a"); // false
// \D = bukan digit
/\D/.test("a"); // true
// \w = word character (a-z, A-Z, 0-9, _)
/\w/.test("a"); // true
/\w/.test("!"); // false
// \s = whitespace (spasi, tab, newline)
/\s/.test(" "); // true
/\s/.test("\t"); // true
// \b = word boundary — posisi antara \w dan \W
/\bkata\b/.test("ini kata bukan"); // true — "kata" berdiri sendiri
/\bkata\b/.test("katakata"); // false — tidak ada batas kata
// ^ = awal string (atau awal baris dengan flag m)
/^halo/.test("halo dunia"); // true
/^halo/.test("hai halo"); // false
// $ = akhir string (atau akhir baris dengan flag m)
/dunia$/.test("halo dunia"); // true
/dunia$/.test("dunia baru"); // false
Quantifier #
// Quantifier mengontrol berapa kali elemen sebelumnya boleh muncul
// * = 0 atau lebih
// + = 1 atau lebih
// ? = 0 atau 1 (opsional)
// {n} = tepat n kali
// {n,} = minimal n kali
// {n,m} = antara n sampai m kali
/\d*/.test(""); // true — 0 atau lebih digit
/\d+/.test(""); // false — minimal 1 digit
/\d?/.test(""); // true — 0 atau 1 digit
/\d{3}/.test("123"); // true — tepat 3 digit
/\d{2,4}/.test("12"); // true — antara 2-4 digit
/\d{2,4}/.test("12345"); // true — cocok 4 digit di awal
// Greedy vs Lazy — greedy mengambil sebanyak mungkin, lazy sesedikit mungkin
const html = "<b>tebal</b> dan <i>miring</i>";
const greedy = html.match(/<.+>/); // Greedy — cocok "<b>tebal</b> dan <i>miring</i>"
const lazy = html.match(/<.+?>/); // Lazy (?) — cocok "<b>" saja
console.log(greedy?.[0]); // "<b>tebal</b> dan <i>miring</i>"
console.log(lazy?.[0]); // "<b>"
Capturing Group dan Named Group #
Capturing Group ()
#
// Capturing group — tangkap bagian dari kecocokan
const regexJam = /(\d{2}):(\d{2})(?::(\d{2}))?/;
const hasilJam = regexJam.exec("waktu: 14:30:45");
if (hasilJam) {
const [keseluruhan, jam, menit, detik] = hasilJam;
console.log(`Jam: ${jam}, Menit: ${menit}, Detik: ${detik ?? "00"}`);
// "Jam: 14, Menit: 30, Detik: 45"
}
// Non-capturing group (?:) — kelompok tanpa tangkapan
// Berguna untuk grouping tanpa perlu hasilnya
const regexNonCapture = /(?:https?|ftp):\/\/(\w+\.\w+)/;
const hasilURL = regexNonCapture.exec("kunjungi https://contoh.com");
if (hasilURL) {
console.log(hasilURL[0]); // "https://contoh.com" — kecocokan penuh
console.log(hasilURL[1]); // "contoh.com" — hanya domain (grup 1)
// hasilURL[2] tidak ada karena (?:) tidak menangkap
}
Named Capturing Group (?<nama>)
#
Named group memberikan nama deskriptif pada grup tangkapan — lebih mudah dibaca dan tidak perlu menghitung indeks:
// Named group — akses lewat groups.nama, bukan indeks angka
const regexNIK = /^(?<provinsi>\d{2})(?<kotaKab>\d{2})(?<kecamatan>\d{2})(?<tglLahir>\d{6})(?<urutan>\d{4})$/;
const nik = "3171011708950001";
const hasilNIK = regexNIK.exec(nik);
if (hasilNIK?.groups) {
const { provinsi, kotaKab, kecamatan, tglLahir, urutan } = hasilNIK.groups;
console.log(`Provinsi: ${provinsi}`); // "31" (Jakarta)
console.log(`Kota/Kab: ${kotaKab}`); // "71" (Jakarta Selatan)
console.log(`Kecamatan: ${kecamatan}`); // "01"
console.log(`Tgl Lahir: ${tglLahir}`); // "170895" (17 Agustus 1995)
}
// Named group dalam replace — gunakan $<nama>
const tgl = "2025-05-07";
const formatBaru = tgl.replace(
/(?<tahun>\d{4})-(?<bulan>\d{2})-(?<hari>\d{2})/,
"$<hari>/$<bulan>/$<tahun>"
);
console.log(formatBaru); // "07/05/2025"
Lookahead dan Lookbehind #
Lookahead dan lookbehind adalah zero-width assertion — mereka memeriksa konteks tanpa mengonsumsi karakter:
// Lookahead positif (?=...) — cocok jika diikuti oleh pola
const hargaDenganRupiah = "Rp 1000 dan 2000 IDR dan 3000".match(/\d+(?= IDR)/g);
// ["2000"] — hanya angka yang diikuti " IDR"
// Lookahead negatif (?!...) — cocok jika TIDAK diikuti oleh pola
const angkaTanpaIDR = "Rp 1000 dan 2000 IDR dan 3000".match(/\d+(?! IDR)/g);
// ["1000", "200", "3000"] — angka yang tidak diikuti " IDR"
// (perhatikan "200" karena "2000" mengandung "200" yang tidak diikuti " IDR")
// Lookbehind positif (?<=...) — cocok jika didahului oleh pola
const hargaSetelahRp = "Rp 1000 dan 2000 IDR".match(/(?<=Rp )\d+/g);
// ["1000"] — hanya angka yang didahului "Rp "
// Lookbehind negatif (?<!...) — cocok jika TIDAK didahului oleh pola
const angkaTanpaRp = "Rp 1000 dan 2000 IDR".match(/(?<!Rp )\d+/g);
// ["2000"] — angka yang tidak didahului "Rp "
Jebakan Flag g dan lastIndex
#
Ini adalah salah satu sumber bug paling berbahaya dengan regex di TypeScript/JavaScript:
// ANTI-PATTERN: Regex dengan flag g di variabel yang di-reuse
const regexGlobal = /\d+/g;
// Pertama kali berhasil
console.log(regexGlobal.test("abc123")); // true
console.log(regexGlobal.lastIndex); // 6 — regex "ingat" posisi!
// Kedua kali dengan input SAMA — gagal!
console.log(regexGlobal.test("abc123")); // false — karena lastIndex=6, dicari dari posisi 6
console.log(regexGlobal.lastIndex); // 0 — reset setelah gagal
// Ketiga kali — berhasil lagi
console.log(regexGlobal.test("abc123")); // true — mulai dari 0 lagi
// SOLUSI 1: Buat regex baru setiap kali digunakan
function validasiAngka(input: string): boolean {
return /\d+/g.test(input); // Literal baru setiap pemanggilan
}
// SOLUSI 2: Reset lastIndex secara manual
function validasiDenganReset(regex: RegExp, input: string): boolean {
regex.lastIndex = 0;
return regex.test(input);
}
// SOLUSI 3: Gunakan regex TANPA flag g untuk test()
// test() tidak membutuhkan flag g — flag g hanya perlu untuk mencari semua kecocokan
const regexTanpaG = /\d+/; // Tidak ada masalah lastIndex
console.log(regexTanpaG.test("abc123")); // true — selalu
console.log(regexTanpaG.test("abc123")); // true — tidak ada state
Pola Validasi Umum untuk Indonesia #
// Validasi email (sederhana tapi berguna untuk sebagian besar kasus)
const REGEX_EMAIL = /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/;
console.log(REGEX_EMAIL.test("[email protected]")); // true
console.log(REGEX_EMAIL.test("[email protected]")); // false
// Nomor telepon Indonesia
const REGEX_TELEPON_ID = /^(\+62|62|0)[0-9]{8,12}$/;
console.log(REGEX_TELEPON_ID.test("08123456789")); // true
console.log(REGEX_TELEPON_ID.test("+62812345678")); // true
console.log(REGEX_TELEPON_ID.test("628123456789")); // true
console.log(REGEX_TELEPON_ID.test("1234567")); // false
// NIK (Nomor Induk Kependudukan) — 16 digit
const REGEX_NIK = /^\d{16}$/;
console.log(REGEX_NIK.test("3171011708950001")); // true
// NPWP — format xx.xxx.xxx.x-xxx.xxx
const REGEX_NPWP = /^\d{2}\.\d{3}\.\d{3}\.\d-\d{3}\.\d{3}$/;
console.log(REGEX_NPWP.test("12.345.678.9-012.345")); // true
// Kode pos Indonesia — 5 digit
const REGEX_KODE_POS = /^\d{5}$/;
console.log(REGEX_KODE_POS.test("12345")); // true
// URL sederhana
const REGEX_URL = /^https?:\/\/([\w-]+\.)+[\w-]+(\/[\w-./?%&=]*)?$/;
console.log(REGEX_URL.test("https://typescript.unisbadri.com/basic/regex/")); // true
// Password kuat (min 8 karakter, ada huruf besar, kecil, angka, simbol)
const REGEX_PASSWORD_KUAT = /^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[@$!%*?&])[A-Za-z\d@$!%*?&]{8,}$/;
console.log(REGEX_PASSWORD_KUAT.test("P@ssword1")); // true
console.log(REGEX_PASSWORD_KUAT.test("password")); // false
// Slug URL
const REGEX_SLUG = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
console.log(REGEX_SLUG.test("belajar-typescript-dasar")); // true
console.log(REGEX_SLUG.test("Belajar TypeScript")); // false
Anatomi Pola Regex #
flowchart TD
A["Pola Regex"] --> B["Karakter"]
A --> C["Quantifier"]
A --> D["Anchor"]
A --> E["Group"]
A --> F["Lookaround"]
B --> B1["Literal: abc atau 123"]
B --> B2["Metacharacter"]
B --> B3["Kelas karakter"]
B --> B4["Negasi kelas karakter"]
C --> C1["Star = 0 atau lebih"]
C --> C2["Plus = 1 atau lebih"]
C --> C3["Question = 0 atau 1"]
C --> C4["Rentang jumlah karakter"]
C --> C5["Lazy quantifier"]
D --> D1["Awal string atau baris"]
D --> D2["Akhir string atau baris"]
D --> D3["Batas kata"]
E --> E1["Capturing group"]
E --> E2["Named group"]
E --> E3["Non capturing group"]
E --> E4["Alternation"]
F --> F1["Lookahead"]
F --> F2["Negative lookahead"]
F --> F3["Lookbehind"]
F --> F4["Negative lookbehind"]Ringkasan #
- Literal regex
/pola/dikompilasi saat parse time dan lebih efisien; gunakannew RegExp(string)hanya saat pola bersifat dinamis (dari variabel atau input pengguna).test()untuk cek keberadaan pola (return boolean) — paling sederhana dan tidak punya masalahlastIndexjika tanpa flagg.match()tanpa flaggmengembalikanRegExpMatchArray | nulldengan detail grup; dengan flaggmengembalikanstring[] | nulltanpa detail grup.matchAll()(ES2020) memberikan semua kecocokan dengan detail grup sekaligus — ini pilihan terbaik untuk mencari semua kecocokan dengan akses ke captured group.- Jebakan
lastIndex: regex dengan flaggmenyimpan posisi terakhir dalam propertilastIndex; memanggiltest()atauexec()berkali-kali pada input yang sama bisa menghasilkan hasil yang berbeda — selalu buat regex baru atau resetlastIndex = 0sebelum digunakan ulang.- Named capturing group
(?<nama>pola)jauh lebih mudah dibaca dan dirawat dari grup berindeks angka; akses lewathasil.groups?.nama.- Greedy vs lazy: secara default quantifier greedy (
+,*) mengambil sebanyak mungkin — tambahkan?setelah quantifier (+?,*?) untuk lazy yang mengambil sesedikit mungkin.- Lookahead dan lookbehind adalah zero-width assertion yang memeriksa konteks tanpa mengonsumsi karakter — sangat berguna untuk pola kondisional seperti “angka yang didahului Rp” atau “kata yang tidak diikuti tanda titik”.
- Selalu test regex dengan kasus valid, invalid, dan edge case sebelum digunakan di produksi — tools seperti regex101.com sangat membantu untuk debugging pola yang kompleks.