Operators #

Operators are symbols or keywords that tell the compiler to perform a certain operation on one or more operands. TypeScript inherits all JavaScript operators — but the interaction between operators and the TypeScript type system creates a new dimension worth understanding. Some JavaScript operators that look safe can actually hide type bugs; conversely, TypeScript adds special operators like as, satisfies, and keyof that don’t exist in JavaScript at all. This article covers all operators relevant to day-to-day TypeScript development — with special emphasis on traps that often go unnoticed and the unique operators TypeScript adds.

Arithmetic Operators #

Arithmetic operators work on the number and bigint types. TypeScript statically ensures arithmetic operators are only used on compatible types — you can’t accidentally add a string to a number like you can in JavaScript.

let a: number = 20;
let b: number = 6;

console.log(a + b);  // 26  — addition
console.log(a - b);  // 14  — subtraction
console.log(a * b);  // 120 — multiplication
console.log(a / b);  // 3.3333... — division (always float in JS/TS)
console.log(a % b);  // 2   — modulus (remainder)
console.log(a ** b); // 64_000_000 — exponent (power)

// Increment and decrement
let counter = 0;
counter++;  // Post-increment: use the value first, then increment
++counter;  // Pre-increment: increment first, then use the value
counter--;  // Post-decrement
--counter;  // Pre-decrement

Numeric Separators for Readability #

TypeScript supports underscores as thousands separators in number literals — this is only sugar syntax, it doesn’t change the value:

// ANTI-PATTERN: Large numbers without separators — hard to read
const batasTransaksi = 50000000;
const jarakBumi = 149600000000;

// CORRECT: Use underscores as thousands separators
const batasTransaksi2 = 50_000_000;     // 50 million
const jarakBumi2     = 149_600_000_000; // 149.6 billion meters
const satu_kb        = 1_024;
const rgb_merah      = 0xFF_00_00;      // Works in hex too

Arithmetic Traps: Special Values #

TypeScript can’t catch all arithmetic errors at compile time — some only appear at runtime:

// Division by zero — no error, produces Infinity or NaN
console.log(10 / 0);    // Infinity
console.log(-10 / 0);   // -Infinity
console.log(0 / 0);     // NaN

// NaN (Not a Number) is a number in TypeScript — a source of subtle bugs
const hasil: number = NaN;        // ✓ No compilation error
console.log(typeof NaN);          // "number" — confusing, but that's the fact
console.log(NaN === NaN);         // false — NaN isn't equal to itself!

// CORRECT: Check NaN with isNaN() or Number.isNaN()
const inputPengguna = parseFloat("bukan-angka");
if (Number.isNaN(inputPengguna)) {
  console.log("Input bukan angka yang valid");
}

Assignment Operators #

Assignment operators set a value to a variable. TypeScript ensures the assigned value’s type is compatible with the variable’s type.

let skor: number = 100;

skor += 10;  // skor = skor + 10  → 110
skor -= 5;   // skor = skor - 5   → 105
skor *= 2;   // skor = skor * 2   → 210
skor /= 3;   // skor = skor / 3   → 70
skor %= 8;   // skor = skor % 8   → 6
skor **= 2;  // skor = skor ** 2  → 36

Logical Assignment Operators (ES2021) #

TypeScript supports three logical assignment operators that are very useful for initialization patterns:

// ??= : Assign only if the current value is null or undefined
let konfigurasi: string | null = null;
konfigurasi ??= "nilai-default";  // konfigurasi = "nilai-default"
konfigurasi ??= "nilai-lain";     // Unchanged — konfigurasi already has a value

// ||= : Assign if the current value is falsy (null, undefined, 0, "", false)
let nama: string = "";
nama ||= "Anonim";  // nama = "Anonim" because "" is falsy

// &&= : Assign only if the current value is truthy
let pesan: string = "Halo";
pesan &&= pesan.toUpperCase();  // pesan = "HALO" because "Halo" is truthy

let pesanKosong: string = "";
pesanKosong &&= pesanKosong.toUpperCase(); // Unchanged — "" is falsy

Comparison Operators: == vs === #

This is one of the most common bug sources inherited from JavaScript. TypeScript adds extra safety but doesn’t fully prevent the use of ==.

// == (loose equality) — performs type coercion before comparing
console.log(1 == "1");    // true  — the string "1" is converted to the number 1
console.log(0 == false);  // true  — false is converted to 0
console.log(null == undefined); // true — special case

// === (strict equality) — compares value AND type, no coercion
console.log(1 === "1");   // false — different types, immediately false
console.log(0 === false); // false — different types
console.log(null === undefined); // false — different types

TypeScript with strict: true often prevents obviously wrongly-typed == comparisons, but not always:

// TypeScript prevents comparisons that are clearly nonsensical
const angka: number = 42;
// if (angka == "42") {} // ✗ Error: This comparison appears to be unintentional...

// But for union types, == can still slip through — still use ===
let nilai: number | null = null;
if (nilai == null) {  // ✓ Old trick: == null catches both null AND undefined
  console.log("Nilai kosong");
}
// Equivalent to: if (nilai === null || nilai === undefined)
Use === for almost all comparisons. One accepted exception is == null to check both null and undefined at once — but many teams prefer writing the condition explicitly to be clearer.

Relational Operator Comparison Table #

OperatorMeaningExampleResult
==Equal (with coercion)"5" == 5true
===Identical (no coercion)"5" === 5false
!=Not equal (with coercion)"5" != 5false
!==Not identical (no coercion)"5" !== 5true
>Greater than10 > 5true
<Less than3 < 7true
>=Greater than or equal5 >= 5true
<=Less than or equal4 <= 3false

Logical Operators and Short-Circuit Evaluation #

Logical operators in TypeScript don’t just produce boolean — they return one of the operands based on short-circuit evaluation. This is an important behavior often used for concise patterns.

// && (AND) — returns the left operand if falsy, the right operand if the left is truthy
console.log(true && "halo");   // "halo"  — left truthy, return the right
console.log(false && "halo");  // false   — left falsy, return the left
console.log(0 && "halo");      // 0       — 0 falsy, return the left
console.log("ada" && "halo");  // "halo"  — left truthy, return the right

// || (OR) — returns the left operand if truthy, the right operand if the left is falsy
console.log(true || "fallback");   // true      — left truthy, return the left
console.log(false || "fallback");  // "fallback" — left falsy, return the right
console.log(0 || "fallback");      // "fallback" — 0 falsy, return the right
console.log("ada" || "fallback");  // "ada"     — left truthy, return the left

?? — Nullish Coalescing #

?? only returns the right side if the left side is null or undefined — unlike || which returns the right side for all falsy values:

// The crucial difference between || and ??
const stok = 0;

// ANTI-PATTERN: Using || for number fallbacks
const tampilStok1 = stok || "Tidak ada stok";
// "Tidak ada stok" — WRONG! 0 is valid stock, not "no value"

// CORRECT: Using ?? for null/undefined-only fallbacks
const tampilStok2 = stok ?? "Data tidak tersedia";
// 0 — CORRECT! 0 isn't null/undefined, so the original value is kept

// Real example: configuration with default values
interface Konfigurasi {
  timeout?: number;  // might be undefined
  retries?: number;
}

function buatKoneksi(konfig: Konfigurasi): void {
  const timeout = konfig.timeout ?? 5000;   // Default 5 seconds
  const retries = konfig.retries ?? 3;      // Default 3 times

  // With ||, timeout = 0 would be replaced by 5000 — a subtle bug!
  // With ??, timeout = 0 stays 0 — the correct behavior
}

?. — Optional Chaining #

Optional chaining stops evaluation and returns undefined immediately if the operand to the left of ?. is null or undefined:

interface Profil {
  pengguna?: {
    alamat?: {
      kota?: string;
    };
  };
}

const data: Profil = {};

// ANTI-PATTERN: Verbose manual checks
const kota1 =
  data.pengguna !== undefined &&
  data.pengguna.alamat !== undefined &&
  data.pengguna.alamat.kota !== undefined
    ? data.pengguna.alamat.kota
    : undefined;

// CORRECT: Optional chaining — concise and safe
const kota2 = data.pengguna?.alamat?.kota; // undefined — no crash

// Optional chaining also works for function calls
const panjangKota = data.pengguna?.alamat?.kota?.length; // undefined

// And for array element access
const items: string[] | undefined = undefined;
const item = items?.[0]; // undefined — no crash

Type Operators: typeof, instanceof, and in #

These three operators are very important in TypeScript because they serve a dual purpose: besides giving runtime information, they’re also used for type narrowing — narrowing a union type to something more specific within a condition block.

typeof — Narrowing Primitive Types #

function prosesNilai(nilai: string | number | boolean | null): string {
  // typeof works for primitive types
  if (typeof nilai === "string") {
    return nilai.toUpperCase();   // TypeScript knows: nilai is a string here
  }
  if (typeof nilai === "number") {
    return nilai.toFixed(2);      // TypeScript knows: nilai is a number here
  }
  if (typeof nilai === "boolean") {
    return nilai ? "Ya" : "Tidak"; // TypeScript knows: nilai is a boolean
  }
  return "Nilai kosong"; // TypeScript knows: nilai is null here
}

// The typeof result table for common types
// typeof "teks"     → "string"
// typeof 42         → "number"
// typeof true       → "boolean"
// typeof undefined  → "undefined"
// typeof {}         → "object"
// typeof []         → "object"  ← trap! Arrays are "object"
// typeof null       → "object"  ← a historical JavaScript bug
// typeof function(){} → "function"
typeof null === "object" is a historical JavaScript bug that can’t be fixed because it would break backward compatibility. To check for null, always use === null explicitly, not typeof.

instanceof — Narrowing Classes and Objects #

instanceof checks whether an object is an instance of a certain class — it walks the prototype chain:

class HewanPeliharaan {
  constructor(public nama: string) {}
}

class Kucing extends HewanPeliharaan {
  mengeong(): void { console.log("Meow!"); }
}

class Anjing extends HewanPeliharaan {
  menggonggong(): void { console.log("Woof!"); }
}

function suaraHewan(hewan: Kucing | Anjing): void {
  // instanceof for class narrowing
  if (hewan instanceof Kucing) {
    hewan.mengeong();      // TypeScript knows: hewan is a Kucing here
  } else {
    hewan.menggonggong();  // TypeScript knows: hewan is an Anjing here
  }
}

// instanceof is also useful for error handling
try {
  JSON.parse("invalid");
} catch (error) {
  if (error instanceof SyntaxError) {
    console.log("JSON tidak valid:", error.message);
  } else if (error instanceof Error) {
    console.log("Error lain:", error.message);
  }
}

in — Narrowing Based on Properties #

The in operator checks whether a property exists in an object — very useful for narrowing discriminated unions:

interface KuciPersegi {
  tipe: "persegi";
  sisi: number;
}

interface KuciiLingkaran {
  tipe: "lingkaran";
  jariJari: number;
}

type Bentuk = KuciPersegi | KuciiLingkaran;

function hitungLuas(bentuk: Bentuk): number {
  // Narrowing with 'in' — check for the existence of a specific property
  if ("sisi" in bentuk) {
    return bentuk.sisi ** 2; // TypeScript knows: bentuk is a KuciPersegi
  }
  return Math.PI * bentuk.jariJari ** 2; // TypeScript knows: bentuk is a KuciiLingkaran
}

Ternary and Nested Ternary Operators #

The ternary operator is a one-line conditional expression: condition ? valueIf_true : valueIf_false. TypeScript infers the ternary result type from both branches:

const usia: number = 17;

// Simple ternary — TypeScript infers the string type
const status = usia >= 18 ? "Dewasa" : "Belum dewasa";
// Type of status: string

// Ternary with different types — TypeScript creates a union
const nilai = usia >= 18 ? 100 : null;
// Type of nilai: number | null

// ANTI-PATTERN: Hard-to-read nested ternaries
const label = usia < 13 ? "Anak" : usia < 18 ? "Remaja" : usia < 60 ? "Dewasa" : "Lansia";
// Hard to read, hard to debug

// CORRECT: Use a function or if-else for multi-level conditions
function kategorikanUsia(usia: number): string {
  if (usia < 13) return "Anak";
  if (usia < 18) return "Remaja";
  if (usia < 60) return "Dewasa";
  return "Lansia";
}

Bitwise Operators #

Bitwise operators work on the 32-bit binary representation of numbers. Rarely used in general application development, but important for binary data processing, permission flags, and certain performance optimizations.

const a: number = 5;  // Binary: 0000 0101
const b: number = 3;  // Binary: 0000 0011

console.log(a & b);   // AND: 0000 0001 = 1
console.log(a | b);   // OR:  0000 0111 = 7
console.log(a ^ b);   // XOR: 0000 0110 = 6
console.log(~a);       // NOT: 1111 1010 = -6 (two's complement)
console.log(a << 1);  // Left shift:  0000 1010 = 10 (× 2)
console.log(a >> 1);  // Right shift: 0000 0010 = 2  (÷ 2)
console.log(a >>> 1); // Unsigned right shift: 2 (same for positive numbers)

A Common Bitwise Pattern: Permission Flags #

// Permission flags pattern — each bit represents one permission
const IZIN_BACA   = 0b0001; // 1
const IZIN_TULIS  = 0b0010; // 2
const IZIN_HAPUS  = 0b0100; // 4
const IZIN_ADMIN  = 0b1000; // 8

// Combine permissions with OR
const izinEditor = IZIN_BACA | IZIN_TULIS;         // 0011 = 3
const izinAdmin  = IZIN_BACA | IZIN_TULIS | IZIN_HAPUS | IZIN_ADMIN; // 1111 = 15

// Check permissions with AND
function punyaIzin(izinPengguna: number, izinDicek: number): boolean {
  return (izinPengguna & izinDicek) === izinDicek;
}

console.log(punyaIzin(izinEditor, IZIN_BACA));   // true
console.log(punyaIzin(izinEditor, IZIN_HAPUS));  // false
console.log(punyaIzin(izinAdmin, IZIN_ADMIN));   // true

TypeScript-Specific Operators #

Besides JavaScript operators, TypeScript adds several operators that only exist at the type system level.

as — Type Assertions #

as tells TypeScript to treat a value as a certain type, overriding the compiler’s inference. This is a “trust me” operation that’s prone to misuse:

// Legitimate case: narrowing a type known to be more specific
const inputElemen = document.getElementById("nama") as HTMLInputElement;
console.log(inputElemen.value); // ✓ TypeScript knows this is HTMLInputElement, not HTMLElement

// ANTI-PATTERN: Misusing as to force an incompatible type
const angka = "bukan angka" as unknown as number;
// Two as's: first to unknown (passes), then to number — this is "forcing" an assertion
// No compilation error, but the logic is totally wrong

satisfies — Type Validation Without Losing Inference (TypeScript 4.9+) #

satisfies is a safer operator than as — it validates that a value is compatible with a certain type, but still preserves the most specific type the compiler inferred:

type PaletWarna = {
  [kunci: string]: string | [number, number, number];
};

// With a regular type annotation — the specific type is lost
const palet1: PaletWarna = {
  merah: [255, 0, 0],
  hijau: "#00FF00",
};
// palet1.merah — type: string | [number, number, number]
// Can't use .length on the tuple directly

// With satisfies — the specific type is preserved
const palet2 = {
  merah: [255, 0, 0],
  hijau: "#00FF00",
} satisfies PaletWarna;
// palet2.merah — type: [number, number, number] — TypeScript knows this is a tuple!
// palet2.hijau — type: string — TypeScript knows this is a string!
palet2.merah[0]; // ✓ Access tuple elements directly
palet2.hijau.toUpperCase(); // ✓ String methods directly

keyof and typeof at the Type Level #

TypeScript also uses typeof and keyof as type operators — different from the runtime operators:

const konfigurasi = {
  host: "localhost",
  port: 5432,
  ssl: false,
};

// typeof at the type level — extract the type from a value
type TipeKonfigurasi = typeof konfigurasi;
// { host: string; port: number; ssl: boolean }

// keyof — extract the union of all keys of a type
type KunciKonfigurasi = keyof TipeKonfigurasi;
// "host" | "port" | "ssl"

// A very useful combination
function ambilKonfigurasi<K extends keyof typeof konfigurasi>(
  kunci: K
): typeof konfigurasi[K] {
  return konfigurasi[kunci];
}

const host = ambilKonfigurasi("host"); // Type: string
const port = ambilKonfigurasi("port"); // Type: number
// ambilKonfigurasi("invalid");         // ✗ Error — key doesn't exist

Operator Precedence #

Operators are evaluated by precedence order — higher-precedence operators are evaluated first. The table below is ordered from highest to lowest precedence:

PrecedenceOperatorExample
Highest() grouping(a + b) * c
?. optional chainingobj?.prop
** exponent2 ** 10
!, ~, ++, --, typeof!aktif, typeof x
*, /, %a * b / c
+, -a + b
<<, >>, >>>a << 2
<, >, <=, >=, in, instanceofa > b
==, !=, ===, !==a === b
& bitwise ANDa & b
^ bitwise XORa ^ b
| bitwise ORa | b
&& logical ANDa && b
|| logical ORa || b
?? nullish coalescinga ?? b
?: ternarya ? b : c
Lowest=, +=, -=, ??=, etc.a = b

Summary #

  • Always use === instead of == — strict equality performs no type coercion and is far more predictable; the single accepted exception is == null to catch both null and undefined at once.
  • ?? is more precise than || for fallbacks — nullish coalescing only replaces null and undefined, while || replaces all falsy values including 0, "", and false, which are often valid values.
  • ?. for safe nested property access — optional chaining removes the need for verbose manual null checks and prevents runtime TypeErrors.
  • typeof, instanceof, in aren’t just runtime operators — in TypeScript, all three also act as type guards that narrow union types within condition blocks.
  • typeof null === "object" is a historical bug — always check null with === null, not with typeof.
  • ??=, ||=, &&= for concise initialization — logical assignment operators combine a check and an assignment in one more expressive expression.
  • satisfies is safer than as — use satisfies to validate a type while keeping specific inference; as should be treated as a last resort because it bypasses compiler checks.
  • keyof typeof is a powerful combination — dynamically extract the key-type union from a value, useful for making type-safe functions with non-hardcoded keys.
  • Bitwise operators for permission flags — the flags & FLAG_X pattern is an efficient way to manage layered permissions in a single integer.

← Previous: Data Types   Next: Conditional Branching →

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