Strings #
String is the data type you’ll encounter most often in almost every program — from processing user input, building queries, formatting messages, to manipulating URLs and paths. TypeScript inherits JavaScript’s very rich string capabilities, plus a type system that ensures you don’t accidentally call string methods on values that aren’t strings. Understanding the built-in string methods deeply — when to use which, and which traps to avoid — is a foundational skill that directly impacts everyday code quality.
Creating Strings #
TypeScript supports three ways of writing string literals, each with its own characteristics.
// single quotes and double quotes — functionally identical
const nama = 'Budi Santoso';
const kota = "Jakarta";
// template literal (backtick) — supports interpolation and multiline
const sapaan = `Halo, ${nama}! Kamu tinggal di ${kota}.`;
// multiline with a template literal — no \n needed
const pesan = `
Selamat datang, ${nama}.
Akun kamu telah aktif.
Silakan login untuk melanjutkan.
`.trim();
// ANTI-PATTERN: string concatenation for multiline
const pesanSalah =
"Selamat datang, " + nama + ".\n" +
"Akun kamu telah aktif.\n" + // ✗ hard to read, prone to errors
"Silakan login untuk melanjutkan.";
// CORRECT: a template literal is far cleaner
const pesanBenar = `Selamat datang, ${nama}.
Akun kamu telah aktif.
Silakan login untuk melanjutkan.`;
Strings in JavaScript and TypeScript are immutable — every operation that seems to “change” a string actually creates a new string. This matters when doing many string operations in a loop.
// strings are immutable — s doesn't change
let s = "hello";
s.toUpperCase(); // ✗ doesn't change s
console.log(s); // "hello" — still the same
// CORRECT: store the result
const upper = s.toUpperCase();
console.log(upper); // "HELLO"
Length and Character Access #
const teks = "TypeScript";
// length — the number of characters (code units, not code points)
console.log(teks.length); // 10
// character access by index
console.log(teks[0]); // "T"
console.log(teks.at(0)); // "T" — same as [0]
console.log(teks.at(-1)); // "t" — negative index from the end
console.log(teks.at(-3)); // "i" — three from the end
// ANTI-PATTERN: negative index access with bracket notation
console.log(teks[-1]); // undefined — not "t" ✗
// charAt — like bracket notation but returns "" when out of bounds
console.log(teks.charAt(0)); // "T"
console.log(teks.charAt(100)); // "" (empty string)
console.log(teks[100]); // undefined
The at() method was introduced in ES2022 and is a cleaner way to access characters from the end — use it instead of teks[teks.length - 1].
Search and Checking #
Checking for Substring Existence #
const kalimat = "TypeScript adalah bahasa pemrograman yang kuat.";
// includes — does the substring exist? (case-sensitive)
console.log(kalimat.includes("TypeScript")); // true
console.log(kalimat.includes("typescript")); // false — case-sensitive
console.log(kalimat.includes("bahasa", 20)); // true — search starting at index 20
// startsWith and endsWith
console.log(kalimat.startsWith("TypeScript")); // true
console.log(kalimat.startsWith("bahasa", 22)); // true — start at index 22
console.log(kalimat.endsWith("kuat.")); // true
console.log(kalimat.endsWith("kuat", 44)); // true — treat the string as ending at index 44
// ANTI-PATTERN: using indexOf to check existence
if (kalimat.indexOf("TypeScript") !== -1) { // ✗ verbose and not expressive
console.log("ada");
}
// CORRECT: use includes for readability
if (kalimat.includes("TypeScript")) { // ✓ clearer intent
console.log("ada");
}
Finding Positions #
const teks = "satu dua tiga dua satu";
// indexOf — the first position found, -1 if not present
console.log(teks.indexOf("dua")); // 5
console.log(teks.indexOf("dua", 6)); // 14 — search starting at index 6
console.log(teks.indexOf("empat")); // -1
// lastIndexOf — the last position found
console.log(teks.lastIndexOf("dua")); // 14
console.log(teks.lastIndexOf("satu")); // 17
// search — search with a regex, returns the position or -1
console.log(teks.search(/dua/)); // 5
console.log(teks.search(/[0-9]+/)); // -1 (no digits)
// match — returns regex search results
const angka = "Harga: 15000 dan 25000";
const hasilMatch = angka.match(/\d+/g); // the g flag for all occurrences
console.log(hasilMatch); // ["15000", "25000"]
// matchAll — an iterator for all occurrences with capture groups
const teksLink = "Kunjungi example.com dan test.org";
const regexDomain = /(\w+)\.(\w+)/g;
for (const match of teksLink.matchAll(regexDomain)) {
console.log(`Domain: ${match[0]}, nama: ${match[1]}, tld: ${match[2]}`);
}
// Domain: example.com, nama: example, tld: com
// Domain: test.org, nama: test, tld: org
Extracting Substrings #
const teks = "Pemrograman TypeScript";
// 0123456789...
// slice(start, end) — end is exclusive, supports negative indices
console.log(teks.slice(0, 11)); // "Pemrograman"
console.log(teks.slice(12)); // "TypeScript"
console.log(teks.slice(-10)); // "TypeScript" — 10 characters from the end
console.log(teks.slice(-10, -6)); // "Type"
// substring(start, end) — like slice but doesn't support negative indices
console.log(teks.substring(0, 11)); // "Pemrograman"
console.log(teks.substring(12)); // "TypeScript"
console.log(teks.substring(-5)); // treated as 0 — returns the whole string
// ANTI-PATTERN: using the deprecated substr()
console.log(teks.substr(12, 10)); // ✗ deprecated — don't use
// CORRECT: use slice() — more consistent and supports negative indices
console.log(teks.slice(12, 22)); // ✓ "TypeScript"
Choose slice() over substring() for consistency — slice() supports negative indices and behaves more predictably with invalid arguments.
Text Transformation #
Capitalization #
const teks = "hELLO wORLD";
// toUpperCase and toLowerCase
console.log(teks.toUpperCase()); // "HELLO WORLD"
console.log(teks.toLowerCase()); // "hello world"
// capitalize the first letter — no built-in method, make your own
function capitalize(str: string): string {
if (!str) return str;
return str.charAt(0).toUpperCase() + str.slice(1).toLowerCase();
}
console.log(capitalize("typeScript")); // "Typescript"
// title case — capitalize every word
function toTitleCase(str: string): string {
return str
.toLowerCase()
.split(" ")
.map((kata) => kata.charAt(0).toUpperCase() + kata.slice(1))
.join(" ");
}
console.log(toTitleCase("budi santoso dari jakarta")); // "Budi Santoso Dari Jakarta"
// camelCase to snake_case — common for field name conversion
function toSnakeCase(str: string): string {
return str
.replace(/([A-Z])/g, "_$1")
.toLowerCase()
.replace(/^_/, ""); // remove a leading underscore if present
}
console.log(toSnakeCase("namaLengkap")); // "nama_lengkap"
console.log(toSnakeCase("createdAtDate")); // "created_at_date"
// snake_case to camelCase
function toCamelCase(str: string): string {
return str.replace(/_([a-z])/g, (_, huruf) => huruf.toUpperCase());
}
console.log(toCamelCase("nama_lengkap")); // "namaLengkap"
console.log(toCamelCase("created_at_date")); // "createdAtDate"
Trim — Removing Whitespace #
const input = " Budi Santoso ";
// trim — remove whitespace from both sides
console.log(input.trim()); // "Budi Santoso"
// trimStart / trimEnd — remove whitespace from one side
console.log(input.trimStart()); // "Budi Santoso "
console.log(input.trimEnd()); // " Budi Santoso"
// ANTI-PATTERN: using the deprecated trimLeft/trimRight
console.log(input.trimLeft()); // ✗ deprecated
console.log(input.trimRight()); // ✗ deprecated
// trim for form input — a very common pattern
function bersihkanInput(nilai: string): string {
return nilai.trim().replace(/\s+/g, " "); // also normalizes inner spaces
}
console.log(bersihkanInput(" Budi Santoso ")); // "Budi Santoso"
Padding and Filling #
// padStart — add characters at the start until a given length
console.log("5".padStart(3, "0")); // "005"
console.log("42".padStart(5, "0")); // "00042"
console.log("abc".padStart(6)); // " abc" — space padding by default
// padEnd — add characters at the end
console.log("Budi".padEnd(10, ".")); // "Budi......"
console.log("100".padEnd(6, "0")); // "100000"
// use case: formatting invoice numbers or codes
function formatNomorInvoice(nomor: number): string {
return `INV-${String(nomor).padStart(6, "0")}`;
}
console.log(formatNomorInvoice(1)); // "INV-000001"
console.log(formatNomorInvoice(1234)); // "INV-001234"
// use case: display a simple text table
const data = [
["Nama", "Umur", "Kota"],
["Budi", "28", "Jakarta"],
["Sari", "25", "Bandung"],
];
for (const baris of data) {
console.log(
baris[0].padEnd(15) +
baris[1].padEnd(8) +
baris[2]
);
}
// Nama Umur Kota
// Budi 28 Jakarta
// Sari 25 Bandung
Repeat and Repetition #
// repeat — repeat a string N times
console.log("ab".repeat(3)); // "ababab"
console.log("-".repeat(40)); // "----------------------------------------"
console.log(" ".repeat(4)); // " " (8 spaces for indentation)
// use case: separators and indentation
function cetakJudul(judul: string): void {
const separator = "=".repeat(judul.length + 4);
console.log(separator);
console.log(`= ${judul} =`);
console.log(separator);
}
cetakJudul("Laporan Penjualan");
// ====================
// = Laporan Penjualan =
// ====================
Text Replacement #
replace and replaceAll #
const teks = "Saya suka kopi. Kamu suka kopi juga?";
// replace — only replaces the FIRST occurrence
console.log(teks.replace("kopi", "teh"));
// "Saya suka teh. Kamu suka kopi juga?" — the second kopi isn't replaced
// ANTI-PATTERN: using replace to replace all occurrences
console.log(teks.replace(/kopi/g, "teh")); // ✓ with the g regex flag — works, but verbose
// CORRECT: use replaceAll to replace every occurrence
console.log(teks.replaceAll("kopi", "teh"));
// "Saya suka teh. Kamu suka teh juga?"
// replace with a function — dynamic transformation
const teksAngka = "Harga: 15000, Diskon: 2000, Total: 13000";
const hasilFormat = teksAngka.replace(/\d+/g, (angka) => {
return Number(angka).toLocaleString("id-ID");
});
console.log(hasilFormat);
// "Harga: 15.000, Diskon: 2.000, Total: 13.000"
// replace with capture groups — very powerful for text transformation
const tanggalUS = "2024-01-15"; // ISO format: YYYY-MM-DD
const tanggalID = tanggalUS.replace(
/(\d{4})-(\d{2})-(\d{2})/,
"$3/$2/$1" // DD/MM/YYYY
);
console.log(tanggalID); // "15/01/2024"
// HTML sanitization — replace special characters
function escapeHtml(teks: string): string {
return teks
.replaceAll("&", "&")
.replaceAll("<", "<")
.replaceAll(">", ">")
.replaceAll('"', """)
.replaceAll("'", "'");
}
console.log(escapeHtml('<script>alert("xss")</script>'));
// "<script>alert("xss")</script>"
Split and Join #
split and join often work in pairs — breaking a string into an array, processing each element, then joining them back together.
const kalimat = "TypeScript adalah bahasa yang kuat dan ekspresif";
// split by a string
const kata = kalimat.split(" ");
// ["TypeScript", "adalah", "bahasa", "yang", "kuat", "dan", "ekspresif"]
// split with a limit — at most N elements
const duaKata = kalimat.split(" ", 2);
// ["TypeScript", "adalah"]
// split by a regex
const csvLine = "Budi,Santoso,,Jakarta,28";
const kolom = csvLine.split(",");
// ["Budi", "Santoso", "", "Jakarta", "28"]
// split every character
const huruf = "TypeScript".split("");
// ["T", "y", "p", "e", "S", "c", "r", "i", "p", "t"]
// join — combine an array into a string
const tags = ["typescript", "nodejs", "backend"];
console.log(tags.join(", ")); // "typescript, nodejs, backend"
console.log(tags.join(" | ")); // "typescript | nodejs | backend"
console.log(tags.join("")); // "typescriptnodejsbackend"
// common pattern: split → transform → join
const slug = "Panduan TypeScript untuk Pemula"
.toLowerCase()
.trim()
.replace(/[^\w\s-]/g, "") // remove non-alphanumeric characters
.replace(/\s+/g, "-") // replace spaces with dashes
.replace(/-+/g, "-"); // normalize repeated dashes
console.log(slug); // "panduan-typescript-untuk-pemula"
Advanced Template Literals #
Tagged Template Literals #
Tagged templates are a rarely used but very powerful feature — they let you process template literals with a custom function.
// a tag function receives an array of strings and the interpolated values
function highlight(strings: TemplateStringsArray, ...values: unknown[]): string {
return strings.reduce((hasil, str, i) => {
const nilai = values[i - 1];
return hasil + `**${nilai}**` + str;
});
}
const nama = "Budi";
const skor = 95;
console.log(highlight`Selamat, ${nama}! Skor kamu adalah ${skor}.`);
// "Selamat, **Budi**! Skor kamu adalah **95**."
// real use case: SQL queries safe from injection
function sql(strings: TemplateStringsArray, ...values: unknown[]): {
query: string;
params: unknown[];
} {
const params: unknown[] = [];
const query = strings.reduce((hasil, str, i) => {
if (i > 0) {
params.push(values[i - 1]);
return hasil + `$${params.length}` + str; // PostgreSQL placeholder
}
return str;
});
return { query, params };
}
const userId = 42;
const status = "aktif";
const { query, params } = sql`
SELECT * FROM users
WHERE id = ${userId}
AND status = ${status}
`;
console.log(query);
// SELECT * FROM users WHERE id = $1 AND status = $2
console.log(params);
// [42, "aktif"]
String Types in TypeScript #
TypeScript has a unique feature that doesn’t exist in plain JavaScript — the ability to manipulate strings at the type level.
// Utility types for strings
type NamaEvent = "klik" | "hover" | "fokus";
// Capitalize — first letter capitalized
type EventCapitalized = Capitalize<NamaEvent>;
// "Klik" | "Hover" | "Fokus"
// Uncapitalize — first letter lowercase
type EventLower = Uncapitalize<"Klik" | "Hover">;
// "klik" | "hover"
// Uppercase — all uppercase
type EventUpper = Uppercase<NamaEvent>;
// "KLIK" | "HOVER" | "FOKUS"
// Lowercase — all lowercase
type EventLower2 = Lowercase<"KLIK" | "HOVER">;
// "klik" | "hover"
// Template literal types — build union types from string combinations
type Sisi = "atas" | "bawah" | "kiri" | "kanan";
type PropertyMargin = `margin-${Sisi}`;
// "margin-atas" | "margin-bawah" | "margin-kiri" | "margin-kanan"
type PropCSS = `${"padding" | "margin"}-${Sisi}`;
// "padding-atas" | "padding-bawah" | ... | "margin-atas" | ...
// Use for type-safe CSS properties or event handler names
type NamaHandler = `on${Capitalize<NamaEvent>}`;
// "onKlik" | "onHover" | "onFokus"
interface KomponenProps {
onKlik?: () => void;
onHover?: () => void;
onFokus?: () => void;
}
Conversion and Parsing #
// number to string
const angka = 12345.678;
console.log(String(angka)); // "12345.678"
console.log(angka.toString()); // "12345.678"
console.log(angka.toString(2)); // binary: "11000000111001.1010..."
console.log(angka.toString(16)); // hex: "3039.ad..."
console.log(angka.toFixed(2)); // "12345.68" — 2 decimals
console.log(angka.toPrecision(6)); // "12345.7" — 6 significant digits
console.log(angka.toExponential(2)); // "1.23e+4" — scientific notation
// format numbers with a locale
console.log(angka.toLocaleString("id-ID"));
// "12.345,678" — Indonesian format
console.log(angka.toLocaleString("id-ID", {
style: "currency",
currency: "IDR",
minimumFractionDigits: 0,
}));
// "Rp 12.346"
// string to number
console.log(Number("42")); // 42
console.log(Number("3.14")); // 3.14
console.log(Number("")); // 0
console.log(Number("abc")); // NaN
console.log(parseInt("42px")); // 42 — take the number at the start
console.log(parseFloat("3.14em")); // 3.14
console.log(parseInt("0xFF", 16)); // 255 — parse hex
// ANTI-PATTERN: using the unary + for conversion
const hasil = +"42"; // ✗ unclear intent
// CORRECT: Number() is more explicit
const hasilBenar = Number("42"); // ✓ clearly a number conversion
// check whether a string is a valid number
function isNumeric(str: string): boolean {
return !isNaN(Number(str)) && str.trim() !== "";
}
console.log(isNumeric("42")); // true
console.log(isNumeric("3.14")); // true
console.log(isNumeric("abc")); // false
console.log(isNumeric("")); // false
Unicode and Special Characters #
// charCodeAt — the UTF-16 code unit
console.log("A".charCodeAt(0)); // 65
console.log("a".charCodeAt(0)); // 97
// codePointAt — the Unicode code point (more accurate for emoji/characters outside the BMP)
console.log("😀".codePointAt(0)); // 128512
console.log("A".codePointAt(0)); // 65
// fromCharCode and fromCodePoint — create strings from codes
console.log(String.fromCharCode(65, 66, 67)); // "ABC"
console.log(String.fromCodePoint(128512)); // "😀"
// Unicode normalization — important for multilingual string comparison
const s1 = "café"; // é as a single character
const s2 = "cafe\u0301"; // e + combining accent
console.log(s1 === s2); // false — different representations
console.log(s1.normalize() === s2.normalize()); // true — after NFC normalization
// string length with emoji — a common problem
const emoji = "😀";
console.log(emoji.length); // 2 — because an emoji is a surrogate pair (2 code units)
console.log([...emoji].length); // 1 — the correct length using spread/iterator
// iterate strings correctly for characters outside the BMP
const teksEmoji = "Halo 😀!";
for (const char of teksEmoji) {
console.log(char); // iterates per code point, not code unit
}
// H, a, l, o, ' ', 😀, !
When working with strings that may contain emoji or characters outside the BMP (Basic Multilingual Plane), use[...str].lengthorArray.from(str).lengthto get an accurate length based on code points, not.lengthwhich counts UTF-16 code units.
Common Patterns in Real Applications #
Input Validation and Sanitization #
// simple email validation
function isEmailValid(email: string): boolean {
const regex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
return regex.test(email.trim());
}
// Indonesian phone number validation
function isPhoneValid(phone: string): boolean {
const normalized = phone.replace(/[\s\-\(\)]/g, ""); // remove separators
return /^(\+62|62|0)[0-9]{8,12}$/.test(normalized);
}
// normalize a phone number to the +62 format
function normalizePhone(phone: string): string {
const cleaned = phone.replace(/[\s\-\(\)]/g, "");
if (cleaned.startsWith("0")) return "+62" + cleaned.slice(1);
if (cleaned.startsWith("62")) return "+" + cleaned;
return cleaned; // already +62 or another format
}
console.log(normalizePhone("081234567890")); // "+6281234567890"
console.log(normalizePhone("6281234567890")); // "+6281234567890"
Building URLs and Query Strings #
// build a URL with query parameters
function buildURL(base: string, params: Record<string, string | number | boolean>): string {
const url = new URL(base);
for (const [key, value] of Object.entries(params)) {
if (value !== undefined && value !== null && value !== "") {
url.searchParams.set(key, String(value));
}
}
return url.toString();
}
console.log(buildURL("https://api.example.com/produk", {
q: "laptop gaming",
kategori: "elektronik",
hargaMax: 10000000,
halaman: 1,
}));
// "https://api.example.com/produk?q=laptop+gaming&kategori=elektronik&hargaMax=10000000&halaman=1"
// truncate text for previews
function truncate(teks: string, maxLength: number, suffix = "..."): string {
if (teks.length <= maxLength) return teks;
return teks.slice(0, maxLength - suffix.length).trimEnd() + suffix;
}
console.log(truncate("Ini adalah teks yang sangat panjang sekali", 20));
// "Ini adalah teks..."
// highlight keywords in text (for search results)
function highlightKeyword(teks: string, keyword: string): string {
const regex = new RegExp(`(${keyword.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")})`, "gi");
return teks.replace(regex, "<mark>$1</mark>");
}
console.log(highlightKeyword("TypeScript sangat kuat dan TypeScript sangat populer", "typescript"));
// "<mark>TypeScript</mark> sangat kuat dan <mark>TypeScript</mark> sangat populer"
Interpolation and Message Formatting #
// format messages with placeholders — useful for i18n
function format(template: string, params: Record<string, string | number>): string {
return template.replace(/\{(\w+)\}/g, (_, key) => {
return key in params ? String(params[key]) : `{${key}}`;
});
}
const template = "Halo, {nama}! Kamu punya {jumlah} pesan baru.";
console.log(format(template, { nama: "Budi", jumlah: 5 }));
// "Halo, Budi! Kamu punya 5 pesan baru."
// simple pluralization
function plural(jumlah: number, singular: string, jamak: string): string {
return `${jumlah} ${jumlah === 1 ? singular : jamak}`;
}
console.log(plural(1, "item", "items")); // "1 item"
console.log(plural(5, "item", "items")); // "5 items"
Summary #
- Use template literals for interpolation and multiline strings — far cleaner than concatenation with
+.at()for character access — supports negative indices (str.at(-1)) and is more expressive thanstr[str.length - 1].includes()notindexOf()for checking substring existence — more explicit and readable.slice()notsubstring()for substring extraction — supports negative indices and behaves more predictably.replaceAll()notreplace(/pattern/g)to replace every occurrence — clearer intent.- Don’t use
substr(),trimLeft(),trimRight()— all three are deprecated; useslice(),trimStart(), andtrimEnd().- Strings are immutable — every method always returns a new string; store the result in a variable.
- Use
[...str].lengthfor the length of strings containing emoji or characters outside the BMP —.lengthcounts code units, not code points.- Tagged template literals enable custom processing — very useful for injection-safe SQL builders or automatically escaped HTML.
- TypeScript template literal types (
Capitalize,Uppercase,Lowercase) enable string manipulation at the type level for stricter type safety.