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:#fff

number — 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 #

TypeCan AcceptUsable Without NarrowingWhen to Use
anyAll values✓ (unsafe)Emergencies, JS migration
unknownAll values✗ (must narrow)External/API data
neverNo valuesExhaustive checks, throws
voidundefinedReturn 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 #

  • number represents all numbers (integer and float); use bigint for integers beyond Number.MAX_SAFE_INTEGER, and avoid direct float money calculations — store values in the smallest unit (cents/points).
  • string supports very flexible template literals; TypeScript also supports template literal types at the type level for expressive type patterns.
  • boolean is simple, but watch out for the difference between boolean and JavaScript’s truthy/falsy values — explicit checks (!== null, .length > 0) are safer than implicit falsy checks.
  • array stores homogeneous elements; use the T[] syntax for readability, Array<T> for more complex generic cases.
  • tuple gives a rigid structure with a type per position; very useful for function return values that return several differently-typed values.
  • any is dangerous — avoid it as much as possible; use unknown instead for data with an unknown type, because unknown forces you to narrow before operating.
  • never is 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.
  • void is the return type of functions without a meaningful value; it differs from undefined in the callback context — a function typed () => void may 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.

← Previous: Constants   Next: Operators →

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