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("<", "&lt;")
    .replaceAll(">", "&gt;")
    .replaceAll('"', "&quot;")
    .replaceAll("'", "&#039;");
}

console.log(escapeHtml('<script>alert("xss")</script>'));
// "&lt;script&gt;alert(&quot;xss&quot;)&lt;/script&gt;"

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].length or Array.from(str).length to get an accurate length based on code points, not .length which 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 than str[str.length - 1].
  • includes() not indexOf() for checking substring existence — more explicit and readable.
  • slice() not substring() for substring extraction — supports negative indices and behaves more predictably.
  • replaceAll() not replace(/pattern/g) to replace every occurrence — clearer intent.
  • Don’t use substr(), trimLeft(), trimRight() — all three are deprecated; use slice(), trimStart(), and trimEnd().
  • Strings are immutable — every method always returns a new string; store the result in a variable.
  • Use [...str].length for the length of strings containing emoji or characters outside the BMP — .length counts 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.

← Previous: Articles & Resources   Next: I/O →

About | Author | Content Scope | Editorial Policy | Privacy Policy | Disclaimer | Contact