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== nullto check bothnullandundefinedat once — but many teams prefer writing the condition explicitly to be clearer.
Relational Operator Comparison Table #
| Operator | Meaning | Example | Result |
|---|---|---|---|
== | Equal (with coercion) | "5" == 5 | true |
=== | Identical (no coercion) | "5" === 5 | false |
!= | Not equal (with coercion) | "5" != 5 | false |
!== | Not identical (no coercion) | "5" !== 5 | true |
> | Greater than | 10 > 5 | true |
< | Less than | 3 < 7 | true |
>= | Greater than or equal | 5 >= 5 | true |
<= | Less than or equal | 4 <= 3 | false |
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=== nullexplicitly, nottypeof.
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:
| Precedence | Operator | Example |
|---|---|---|
| Highest | () grouping | (a + b) * c |
?. optional chaining | obj?.prop | |
** exponent | 2 ** 10 | |
!, ~, ++, --, typeof | !aktif, typeof x | |
*, /, % | a * b / c | |
+, - | a + b | |
<<, >>, >>> | a << 2 | |
<, >, <=, >=, in, instanceof | a > b | |
==, !=, ===, !== | a === b | |
& bitwise AND | a & b | |
^ bitwise XOR | a ^ b | |
| bitwise OR | a | b | |
&& logical AND | a && b | |
|| logical OR | a || b | |
?? nullish coalescing | a ?? b | |
?: ternary | a ? 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== nullto catch both null and undefined at once.??is more precise than||for fallbacks — nullish coalescing only replacesnullandundefined, while||replaces all falsy values including0,"", andfalse, 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,inaren’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 withtypeof.??=,||=,&&=for concise initialization — logical assignment operators combine a check and an assignment in one more expressive expression.satisfiesis safer thanas— usesatisfiesto validate a type while keeping specific inference;asshould be treated as a last resort because it bypasses compiler checks.keyof typeofis 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_Xpattern is an efficient way to manage layered permissions in a single integer.