Data Types #
The type system is the foundation of all of TypeScript. Understanding data types isn’t just about memorizing keywords — it’s about understanding the hierarchy and relationships between types. TypeScript has two large categories: primitive types (simple values that can’t be modified) and structural types (complex values that have properties and methods). On top of those sit special types like any, unknown, never, and void, each with a unique role in the type system. Choosing the right type for every situation is the skill that separates sloppy TypeScript from truly safe, expressive TypeScript.
The TypeScript Type System Map #
Before discussing each one, it’s important to see the big picture of how TypeScript’s types relate to each other:
flowchart TD
A["TypeScript Types"] --> B["Primitives"]
A --> C["Structural"]
A --> D["Special Types"]
A --> E["Composition Types"]
B --> B1["number"]
B --> B2["string"]
B --> B3["boolean"]
B --> B4["bigint"]
B --> B5["symbol"]
C --> C1["object"]
C --> C2["array (T[])"]
C --> C3["tuple"]
C --> C4["enum"]
C --> C5["function"]
D --> D1["any — disables type check"]
D --> D2["unknown — safe any"]
D --> D3["never — impossible value"]
D --> D4["void — no return value"]
D --> D5["null"]
D --> D6["undefined"]
E --> E1["Union — A or B"]
E --> E2["Intersection — A and B"]
E --> E3["Literal Types"]
E --> E4["Template Literal"]
style D1 fill:#ff6b6b,color:#fff
style D2 fill:#51cf66,color:#fff
style D3 fill:#339af0,color:#fffnumber — All Numbers in One Type
#
TypeScript (like JavaScript) doesn’t distinguish between integers and floats — everything is number. Under the hood, all numbers are 64-bit floating-point numbers per the IEEE 754 standard.
let usia: number = 25;
let harga: number = 149_999.99; // Underscore as a thousands separator — more readable
let jarakBumi: number = 1.496e11; // Exponential notation — 149.6 million km
let binerData: number = 0b1010_1010; // Binary literal
let oktalData: number = 0o755; // Octal literal (Unix permissions)
let warna: number = 0xFF5733; // Hexadecimal literal
The Floating-Point Arithmetic Trap #
number inherits the well-known IEEE 754 floating-point weakness — arithmetic results aren’t always exact:
// ANTI-PATTERN: Using number directly for money calculations
console.log(0.1 + 0.2); // 0.30000000000000004 — WRONG!
console.log(0.1 + 0.2 === 0.3); // false — a common trap
// CORRECT: Use integers (cents/points) for money calculations
// Store the price in cents (IDR × 100), do the math, then convert back
const hargaDalamSen = 14999; // Rp 149.99 → 14999 cents
const kuantitas = 3;
const totalSen = hargaDalamSen * kuantitas; // 44997 cents
const totalRupiah = totalSen / 100; // Rp 449.97 — exact
// Or use toFixed() for display (but the result is a string)
console.log((0.1 + 0.2).toFixed(2)); // "0.30" — for display only
bigint — Very Large Integer Numbers
#
For numbers beyond Number.MAX_SAFE_INTEGER (2⁵³ - 1), use bigint:
// Number.MAX_SAFE_INTEGER = 9007199254740991
// Beyond this, number becomes inaccurate
console.log(9007199254740991 + 1); // 9007199254740992 ✓
console.log(9007199254740991 + 2); // 9007199254740992 ✗ — should be 9007199254740993
// bigint — accurate for very large integers
const idTransaksiGlobal: bigint = 9007199254740993n; // 'n' suffix
const totalDanaInvestasi: bigint = 1_000_000_000_000n; // 1 trillion
// bigint can't be mixed with number without explicit conversion
// const hasil = idTransaksiGlobal + 1; // ✗ Error: operator '+' cannot be applied to 'bigint' and 'number'
const hasil = idTransaksiGlobal + 1n; // ✓ Use a bigint literal
string — Text and Template Literals
#
string represents Unicode text. There are three ways to write string literals in TypeScript, each with different use cases:
let namaPengguna: string = "Budi Santoso"; // Double quotes
let kota: string = 'Jakarta Selatan'; // Single quotes
let pesan: string = `Halo, ${namaPengguna}!`; // Template literal (backticks)
// Template literal — the most flexible
const harga = 150_000;
const diskon = 10;
const tagihan = `
Rincian Pesanan:
─────────────────────────
Harga : Rp ${harga.toLocaleString("id-ID")}
Diskon : ${diskon}%
Total : Rp ${(harga * (1 - diskon / 100)).toLocaleString("id-ID")}
`.trim();
Template Literal Types #
TypeScript has a unique feature that allows template literals to be used at the type level, not just as values:
type ArahKardinal = "Utara" | "Selatan" | "Timur" | "Barat";
type TipeJalan = "Jalan" | "Gang" | "Boulevard";
// Template literal type — combine two union types
type Alamat = `${TipeJalan} ${ArahKardinal}`;
// Type: "Jalan Utara" | "Jalan Selatan" | "Jalan Timur" | "Jalan Barat" |
// "Gang Utara" | "Gang Selatan" | ...
let lokasiKantor: Alamat = "Jalan Selatan"; // ✓
// let lokasiSalah: Alamat = "Perumahan Utara"; // ✗ Error
// Useful for event names, CSS class patterns, API route patterns
type HttpEvent = `on${"Get" | "Post" | "Put" | "Delete"}`;
// Type: "onGet" | "onPost" | "onPut" | "onDelete"
boolean — Binary Logic
#
boolean is the simplest type — only true or false. But there are a few important things about booleans in TypeScript:
let isLoading: boolean = false;
let hasPermission: boolean = true;
let canDelete: boolean = false;
// Type inference — TypeScript infers boolean automatically
const isValid = nama.length > 0 && email.includes("@");
// Type: boolean — no explicit annotation needed
The Truthy/Falsy Trap #
TypeScript understands the difference between boolean and JavaScript’s truthy/falsy values. This matters during narrowing:
// ANTI-PATTERN: Using a non-boolean value as a condition directly
// without realizing the implications
function prosesNama(nama: string | null): void {
if (nama) {
// nama could be '' (empty string) which is falsy — does this still pass? No.
// An empty string is falsy, so this block doesn't execute for ""
console.log(nama.toUpperCase());
}
}
// CORRECT: An explicit check matching the intent
function prosesNamaEksplisit(nama: string | null): void {
if (nama !== null && nama.length > 0) {
// Clear: we reject null AND empty strings
console.log(nama.toUpperCase());
}
}
array — Homogeneous Typed Collections
#
Arrays in TypeScript store collections of elements that all share the same type. There are two equivalent syntaxes:
// Syntax 1: type[]
let angka: number[] = [1, 2, 3, 4, 5];
let nama: string[] = ["Budi", "Siti", "Ahmad"];
// Syntax 2: Array<type> — Generic syntax
let angka2: Array<number> = [1, 2, 3];
let namaPengguna: Array<string> = ["Budi", "Siti"];
// Array of objects
interface Produk {
id: number;
nama: string;
harga: number;
}
let katalog: Produk[] = [
{ id: 1, nama: "Kurma Ajwa", harga: 85_000 },
{ id: 2, nama: "Madu Sidr", harga: 250_000 },
];
Array Operations with Type Safety #
TypeScript ensures all array operations are compatible with the element type:
const nilai: number[] = [85, 92, 78, 95, 88];
// All array methods get type safety
const rataRata = nilai.reduce((acc, n) => acc + n, 0) / nilai.length;
const nilaiMaks = Math.max(...nilai);
const lulus = nilai.filter((n) => n >= 75);
const grade = nilai.map((n) => (n >= 90 ? "A" : n >= 75 ? "B" : "C"));
// grade: string[] — TypeScript infers the return type of map
// ANTI-PATTERN: Inserting the wrong type
// nilai.push("seratus"); // ✗ Error: Argument of type 'string' is not assignable to parameter of type 'number'
tuple — Arrays with a Fixed Structure
#
A tuple is an array whose element count is fixed and whose type at each position is predetermined. Unlike a regular array that only cares about the element type, a tuple also cares about position and count.
// Regular array — free count and order, all elements must share a type
const nilaiNilai: number[] = [85, 92, 78]; // Can add or remove
// Tuple — rigid structure, each position has a specific type
let koordinatJakarta: [number, number] = [-6.2088, 106.8456]; // [lat, lon]
let dataKaryawan: [string, number, boolean] = ["Budi", 25, true]; // [name, age, active]
// Tuple destructuring — clear names are better than indices
const [latitude, longitude] = koordinatJakarta;
const [namaKaryawan, usiaKaryawan, isAktif] = dataKaryawan;
// Labeled tuples — TypeScript 4.0+, makes the intent clearer
type Koordinat = [latitude: number, longitude: number];
type EntriLog = [timestamp: Date, level: string, pesan: string];
Named Tuples as Function Return Values #
Tuples are very useful for functions that need to return more than one value with different types — a lightweight alternative to creating a new interface:
// Result/Error pattern — return [data, error] like Go
type HasilAsync<T> = [data: T | null, error: Error | null];
async function ambilPengguna(id: number): Promise<HasilAsync<{ nama: string }>> {
try {
const response = await fetch(`/api/pengguna/${id}`);
const data = await response.json();
return [data, null]; // Success: data exists, error is null
} catch (err) {
return [null, err as Error]; // Failure: data is null, error exists
}
}
// Clean usage
const [pengguna, error] = await ambilPengguna(1);
if (error) {
console.error("Gagal:", error.message);
} else {
console.log("Berhasil:", pengguna?.nama);
}
object — The Basic Structural Type
#
object is the type representing all non-primitive values — including arrays, functions, and object literals. But in practice, object as an annotation type is rarely useful because it’s too wide.
// The 'object' type — too wide, not useful for type checking
let sesuatu: object = { nama: "Budi" };
// sesuatu.nama; // ✗ Error: Property 'nama' does not exist on type 'object'
// More useful: inline object types
let pengguna: { nama: string; usia: number } = {
nama: "Budi",
usia: 25,
};
pengguna.nama; // ✓ — TypeScript knows which properties exist
// Most useful: use an interface or type alias
interface Pengguna {
nama: string;
usia: number;
email?: string;
}
Index Signatures — Objects with Dynamic Keys #
When you don’t know which keys will exist at runtime, use an index signature:
// An object with string keys unknown in advance
interface KamusData {
[kunci: string]: string | number;
}
const metadata: KamusData = {
judul: "Laporan Q3",
tahun: 2025,
penulis: "Tim Riset",
halaman: 42,
};
// Dynamic access — TypeScript knows the value is string | number
const nilaiJudul = metadata["judul"]; // Type: string | number
// Record<K, V> — a cleaner utility type for dictionaries
type KonfigEnv = Record<string, string>;
const env: KonfigEnv = {
NODE_ENV: "production",
DATABASE_URL: "postgres://...",
SECRET_KEY: "abc123",
};
any, unknown, never, void — The Special Types
#
These four types are often confusing, but each has a very different role in the TypeScript type system.
any — Leaving the Type System
#
any disables all type checking for that variable. It can accept all values and can be treated as if it has any property.
// ANTI-PATTERN: Using any as an easy way out
let data: any = ambilDariAPI();
data.apapun.yang.kamu.mau; // No compilation error — but can crash at runtime!
// WHEN any is still acceptable:
// 1. Gradual migration from JavaScript (add @ts-ignore or @ts-nocheck)
// 2. Very dynamic type declarations that truly can't be typed
// 3. In blocks that already have prior runtime validation
unknown — The Safe Type for Unknown Values
#
unknown accepts all values like any, but TypeScript forces you to do type narrowing before you can use it. This is the safe replacement for any.
// CORRECT: Use unknown for external, unvalidated data
function prosesInputEksternal(input: unknown): string {
// Can't use it directly — must narrow first
if (typeof input === "string") {
return input.trim(); // ✓ TypeScript knows this is a string
}
if (typeof input === "number") {
return input.toFixed(2); // ✓ TypeScript knows this is a number
}
if (Array.isArray(input)) {
return input.join(", "); // ✓ TypeScript knows this is an array
}
return String(input); // Safe fallback
}
never — A Value That Can Never Exist
#
never represents a value that can never exist — an impossible condition to reach. It’s useful for two main cases:
// Case 1: A function that never returns normally (always throws or infinite loop)
function panickan(pesan: string): never {
throw new Error(`Fatal: ${pesan}`);
}
function loopSelamanya(): never {
while (true) {
// Doing something forever
}
}
// Case 2: Exhaustive check — ensure all union cases are handled
type Bentuk = "lingkaran" | "persegi" | "segitiga";
function hitungLuas(bentuk: Bentuk, ukuran: number): number {
switch (bentuk) {
case "lingkaran":
return Math.PI * ukuran ** 2;
case "persegi":
return ukuran ** 2;
case "segitiga":
return (ukuran ** 2 * Math.sqrt(3)) / 4;
default:
// If you add a new type to the Bentuk union but forget to handle it here,
// TypeScript will report an error on this line — very useful!
const _exhaustiveCheck: never = bentuk;
throw new Error(`Bentuk tidak dikenal: ${bentuk}`);
}
}
void — No Return Value
#
void is used for the return type of functions that don’t return a meaningful value. A void function may explicitly return undefined, but can’t return any other type.
// Void function — only performs actions, doesn't return a value
function logAktivitas(pengguna: string, aksi: string): void {
const waktu = new Date().toISOString();
console.log(`[${waktu}] ${pengguna}: ${aksi}`);
// No return — or just return;
}
// ANTI-PATTERN: Thinking void and undefined are exactly the same
function contohVoid(): void {
return undefined; // ✓ This is allowed
// return 42; // ✗ Error: Type 'number' is not assignable to type 'void'
}
// An important difference: void callbacks may ignore the return value
type Callback = () => void;
const cb: Callback = () => 42; // ✓ The return value is ignored
Comparing the Four Special Types #
| Type | Can Accept | Usable Without Narrowing | When to Use |
|---|---|---|---|
any | All values | ✓ (unsafe) | Emergencies, JS migration |
unknown | All values | ✗ (must narrow) | External/API data |
never | No values | — | Exhaustive checks, throws |
void | undefined | — | Return type of valueless functions |
null and undefined — The Absence of Value
#
These two values differ semantically: undefined means “not initialized or absent”, while null means “deliberately no value”. With strictNullChecks on, neither can silently enter other types.
// undefined — the default value when not initialized
let belumDiisi: undefined = undefined;
let hasilKosong: string | undefined; // Not yet filled
// null — deliberate absence of value
let tidakAda: null = null;
let penggunaDitemukan: { nama: string } | null = null; // Not found yet
// Semantic difference in practice
interface Profil {
foto: string | null; // null = deliberately no photo
bio: string | undefined; // undefined = user hasn't filled in a bio
}
Nullish Coalescing and Optional Chaining #
interface Pengguna {
nama: string;
alamat?: {
kota?: string;
provinsi?: string;
};
}
const pengguna: Pengguna = { nama: "Budi" };
// Optional chaining — safely access nested properties that might be undefined
const kota = pengguna.alamat?.kota; // undefined (not an error)
const panjangKota = pengguna.alamat?.kota?.length; // undefined
// Nullish coalescing — fallback for null OR undefined (not 0 or "")
const kotaTampil = pengguna.alamat?.kota ?? "Kota tidak diketahui";
const stok = 0;
const stokTampil = stok ?? 10; // 0 — because 0 isn't null/undefined
Union Types — Type A or Type B #
Union types allow a value to have more than one possible type. The | operator reads as “or”.
// Union of primitive types
let id: number | string;
id = 123; // ✓
id = "usr-1"; // ✓
// Union with null — a very common pattern
type NilaiOpsional<T> = T | null | undefined;
// Discriminated union — a powerful pattern for state management
type StatusRequest =
| { status: "idle" }
| { status: "loading" }
| { status: "sukses"; data: string[] }
| { status: "error"; pesan: string };
function renderUI(state: StatusRequest): string {
switch (state.status) {
case "idle":
return "Siap";
case "loading":
return "Memuat...";
case "sukses":
return `Data: ${state.data.join(", ")}`; // TypeScript knows state.data exists
case "error":
return `Error: ${state.pesan}`; // TypeScript knows state.pesan exists
}
}
Intersection Types — Type A and Type B #
Intersection types combine several types into one — the value must satisfy all the combined types. The & operator reads as “and”.
interface PunyaNama {
nama: string;
}
interface PunyaUsia {
usia: number;
}
interface PunyaEmail {
email: string;
}
// Intersection — must have all properties from all types
type PenggunaPenuh = PunyaNama & PunyaUsia & PunyaEmail;
const pengguna: PenggunaPenuh = {
nama: "Budi",
usia: 25,
email: "[email protected]",
// Must have every property — none can be missing
};
// Common pattern: adding metadata to an existing type
type DenganTimestamp<T> = T & {
dibuatPada: Date;
diperbarui: Date;
};
type ProdukDenganTimestamp = DenganTimestamp<{
nama: string;
harga: number;
}>;
Union vs Intersection — A Visual Analogy #
Union (A | B) Intersection (A & B)
───────────── ────────────────────
┌───┬───┐ ┌───────────┐
│ A │ B │ │ A │
│ │ │ │ ┌───┐ │
└───┴───┘ │ │ A&B │
The value can be │ │ │ │
in A OR in B │ └───┘ │
│ B │
└──────────┘
The value must be
in A AND in B at once
Summary #
numberrepresents all numbers (integer and float); usebigintfor integers beyondNumber.MAX_SAFE_INTEGER, and avoid direct float money calculations — store values in the smallest unit (cents/points).stringsupports very flexible template literals; TypeScript also supports template literal types at the type level for expressive type patterns.booleanis simple, but watch out for the difference betweenbooleanand JavaScript’s truthy/falsy values — explicit checks (!== null,.length > 0) are safer than implicit falsy checks.arraystores homogeneous elements; use theT[]syntax for readability,Array<T>for more complex generic cases.tuplegives a rigid structure with a type per position; very useful for function return values that return several differently-typed values.anyis dangerous — avoid it as much as possible; useunknowninstead for data with an unknown type, becauseunknownforces you to narrow before operating.neveris used for functions that never return and for exhaustive checks in switch/union — the compiler will remind you if a union case isn’t handled.voidis the return type of functions without a meaningful value; it differs fromundefinedin the callback context — a function typed() => voidmay return a value but the value is ignored.- Union types (
A | B) for values that can be one of several types; use discriminated unions with a literal property to build type-safe state machines.- Intersection types (
A & B) for values that must satisfy all types at once; useful for interface composition and “mixin” patterns.