Tipe Data #

Sistem tipe adalah fondasi dari seluruh TypeScript. Memahami tipe data bukan hanya tentang menghafal kata kunci — tapi tentang memahami hierarki dan hubungan antar tipe. TypeScript memiliki dua kategori besar: tipe primitif (nilai sederhana yang tidak bisa dimodifikasi) dan tipe struktural (nilai kompleks yang memiliki properti dan metode). Di lapis atasnya ada tipe-tipe khusus seperti any, unknown, never, dan void yang memiliki peran unik dalam sistem tipe. Memilih tipe yang tepat untuk setiap situasi adalah keterampilan yang membedakan TypeScript yang ditulis asal-asalan dari TypeScript yang benar-benar aman dan ekspresif.

Peta Sistem Tipe TypeScript #

Sebelum membahas satu per satu, penting untuk melihat gambaran besar bagaimana tipe-tipe di TypeScript saling berhubungan:

flowchart TD
    A["Tipe TypeScript"] --> B["Primitif"]
    A --> C["Struktural"]
    A --> D["Tipe Khusus"]
    A --> E["Tipe Komposisi"]

    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 — menonaktifkan type check"]
    D --> D2["unknown — any yang aman"]
    D --> D3["never — nilai yang mustahil"]
    D --> D4["void — tidak ada return value"]
    D --> D5["null"]
    D --> D6["undefined"]

    E --> E1["Union — A atau B"]
    E --> E2["Intersection — A dan 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 — Semua Angka dalam Satu Tipe #

TypeScript (seperti JavaScript) tidak membedakan antara integer dan float — semuanya adalah number. Di balik layar, semua number adalah bilangan floating-point 64-bit sesuai standar IEEE 754.

let usia: number = 25;
let harga: number = 149_999.99;     // Underscore sebagai pemisah ribuan — lebih mudah dibaca
let jarakBumi: number = 1.496e11;   // Notasi eksponensial — 149.6 juta km
let binerData: number = 0b1010_1010; // Literal biner
let oktalData: number = 0o755;       // Literal oktal (hak akses Unix)
let warna: number = 0xFF5733;        // Literal heksadesimal

Jebakan Aritmatika Floating-Point #

number mewarisi kelemahan floating-point IEEE 754 yang terkenal — hasil aritmatika tidak selalu tepat:

// ANTI-PATTERN: Menggunakan number langsung untuk kalkulasi uang
console.log(0.1 + 0.2);           // 0.30000000000000004 — SALAH!
console.log(0.1 + 0.2 === 0.3);   // false — jebakan umum

// BENAR: Gunakan bilangan bulat (sen/poin) untuk kalkulasi uang
// Simpan harga dalam satuan sen (IDR × 100), lakukan kalkulasi, lalu konversi kembali
const hargaDalamSen = 14999; // Rp 149,99 → 14999 sen
const kuantitas = 3;
const totalSen = hargaDalamSen * kuantitas; // 44997 sen
const totalRupiah = totalSen / 100;          // Rp 449,97 — tepat

// Atau gunakan toFixed() untuk tampilan (tapi hasilnya string)
console.log((0.1 + 0.2).toFixed(2)); // "0.30" — untuk tampilan saja

bigint — Angka Integer Sangat Besar #

Untuk angka yang melampaui batas Number.MAX_SAFE_INTEGER (2⁵³ - 1), gunakan bigint:

// Number.MAX_SAFE_INTEGER = 9007199254740991
// Di luar ini, number tidak akurat
console.log(9007199254740991 + 1); // 9007199254740992 ✓
console.log(9007199254740991 + 2); // 9007199254740992 ✗ — seharusnya 9007199254740993

// bigint — akurat untuk integer sangat besar
const idTransaksiGlobal: bigint = 9007199254740993n; // Suffix 'n'
const totalDanaInvestasi: bigint = 1_000_000_000_000n; // 1 triliun

// bigint tidak bisa dicampur dengan number tanpa konversi eksplisit
// const hasil = idTransaksiGlobal + 1; // ✗ Error: operator '+' cannot be applied to 'bigint' and 'number'
const hasil = idTransaksiGlobal + 1n; // ✓ Gunakan literal bigint

string — Teks dan Template Literal #

string merepresentasikan teks Unicode. Ada tiga cara menulis string literal di TypeScript, masing-masing dengan use case berbeda:

let namaPengguna: string = "Budi Santoso";          // Tanda kutip ganda
let kota: string = 'Jakarta Selatan';                // Tanda kutip tunggal
let pesan: string = `Halo, ${namaPengguna}!`;        // Template literal (backtick)

// Template literal — paling fleksibel
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 memiliki fitur unik yang memungkinkan template literal digunakan di level tipe, bukan hanya nilai:

type ArahKardinal = "Utara" | "Selatan" | "Timur" | "Barat";
type TipeJalan = "Jalan" | "Gang" | "Boulevard";

// Template literal type — kombinasikan dua union type
type Alamat = `${TipeJalan} ${ArahKardinal}`;
// Tipe: "Jalan Utara" | "Jalan Selatan" | "Jalan Timur" | "Jalan Barat" |
//       "Gang Utara" | "Gang Selatan" | ...

let lokasiKantor: Alamat = "Jalan Selatan"; // ✓
// let lokasiSalah: Alamat = "Perumahan Utara"; // ✗ Error

// Berguna untuk event names, CSS class patterns, API route patterns
type HttpEvent = `on${"Get" | "Post" | "Put" | "Delete"}`;
// Tipe: "onGet" | "onPost" | "onPut" | "onDelete"

boolean — Logika Biner #

boolean adalah tipe paling sederhana — hanya true atau false. Tapi ada beberapa hal penting terkait boolean di TypeScript:

let isLoading: boolean = false;
let hasPermission: boolean = true;
let canDelete: boolean = false;

// Type inference — TypeScript menyimpulkan boolean secara otomatis
const isValid = nama.length > 0 && email.includes("@");
// Tipe: boolean — tidak perlu anotasi eksplisit

Jebakan Truthy/Falsy #

TypeScript memahami perbedaan antara boolean dan nilai truthy/falsy JavaScript. Ini penting saat narrowing:

// ANTI-PATTERN: Menggunakan nilai non-boolean sebagai kondisi langsung
// tanpa sadar implikasinya
function prosesNama(nama: string | null): void {
  if (nama) {
    // nama bisa '' (string kosong) yang falsy — masih lolos ke sini? Tidak.
    // String kosong adalah falsy, jadi blok ini tidak dieksekusi untuk ""
    console.log(nama.toUpperCase());
  }
}

// BENAR: Cek yang eksplisit sesuai intent
function prosesNamaEksplisit(nama: string | null): void {
  if (nama !== null && nama.length > 0) {
    // Jelas: kita menolak null DAN string kosong
    console.log(nama.toUpperCase());
  }
}

array — Koleksi Bertipe Homogen #

Array di TypeScript menyimpan koleksi elemen yang semuanya bertipe sama. Ada dua sintaks yang ekuivalen:

// Sintaks 1: tipe[]
let angka: number[] = [1, 2, 3, 4, 5];
let nama: string[] = ["Budi", "Siti", "Ahmad"];

// Sintaks 2: Array<tipe> — Generic syntax
let angka2: Array<number> = [1, 2, 3];
let namaPengguna: Array<string> = ["Budi", "Siti"];

// Array of object
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 },
];

Operasi Array dengan Type Safety #

TypeScript memastikan semua operasi array kompatibel dengan tipe elemennya:

const nilai: number[] = [85, 92, 78, 95, 88];

// Semua method array mendapat 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 menyimpulkan tipe return dari map

// ANTI-PATTERN: Memasukkan tipe yang salah
// nilai.push("seratus"); // ✗ Error: Argument of type 'string' is not assignable to parameter of type 'number'

tuple — Array dengan Struktur Tetap #

Tuple adalah array yang jumlah elemennya tetap dan tipe setiap posisinya sudah ditentukan. Berbeda dari array biasa yang hanya peduli pada tipe elemen, tuple juga peduli pada posisi dan jumlah.

// Array biasa — jumlah dan urutan bebas, semua elemen harus bertipe sama
const nilaiNilai: number[] = [85, 92, 78]; // Boleh tambah atau kurangi

// Tuple — struktur kaku, setiap posisi punya tipe spesifik
let koordinatJakarta: [number, number] = [-6.2088, 106.8456]; // [lat, lon]
let dataKaryawan: [string, number, boolean] = ["Budi", 25, true]; // [nama, usia, aktif]

// Destructuring tuple — nama yang jelas lebih baik dari indeks
const [latitude, longitude] = koordinatJakarta;
const [namaKaryawan, usiaKaryawan, isAktif] = dataKaryawan;

// Tuple dengan label — TypeScript 4.0+, membuat intent lebih jelas
type Koordinat = [latitude: number, longitude: number];
type EntriLog = [timestamp: Date, level: string, pesan: string];

Named Tuple sebagai Return Value Fungsi #

Tuple sangat berguna untuk fungsi yang perlu mengembalikan lebih dari satu nilai dengan tipe berbeda — alternatif ringan dari membuat interface baru:

// Pattern Result/Error — kembalikan [data, error] seperti 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];   // Sukses: data ada, error null
  } catch (err) {
    return [null, err as Error]; // Gagal: data null, error ada
  }
}

// Penggunaan yang bersih
const [pengguna, error] = await ambilPengguna(1);
if (error) {
  console.error("Gagal:", error.message);
} else {
  console.log("Berhasil:", pengguna?.nama);
}

object — Tipe Struktural Dasar #

object adalah tipe yang merepresentasikan semua nilai non-primitif — termasuk array, fungsi, dan object literal. Tapi dalam praktiknya, object sebagai tipe anotasi jarang berguna karena terlalu lebar.

// Tipe 'object' — terlalu lebar, tidak berguna untuk type checking
let sesuatu: object = { nama: "Budi" };
// sesuatu.nama; // ✗ Error: Property 'nama' does not exist on type 'object'

// Lebih berguna: tipe inline object
let pengguna: { nama: string; usia: number } = {
  nama: "Budi",
  usia: 25,
};
pengguna.nama; // ✓ — TypeScript tahu properti yang ada

// Paling berguna: gunakan interface atau type alias
interface Pengguna {
  nama: string;
  usia: number;
  email?: string;
}

Index Signature — Object dengan Kunci Dinamis #

Saat kamu tidak tahu kunci apa saja yang akan ada di runtime, gunakan index signature:

// Object dengan kunci string yang tidak diketahui sebelumnya
interface KamusData {
  [kunci: string]: string | number;
}

const metadata: KamusData = {
  judul: "Laporan Q3",
  tahun: 2025,
  penulis: "Tim Riset",
  halaman: 42,
};

// Akses dinamis — TypeScript tahu nilainya string | number
const nilaiJudul = metadata["judul"]; // Tipe: string | number

// Record<K, V> — utility type yang lebih bersih untuk kamus
type KonfigEnv = Record<string, string>;

const env: KonfigEnv = {
  NODE_ENV: "production",
  DATABASE_URL: "postgres://...",
  SECRET_KEY: "abc123",
};

any, unknown, never, void — Tipe Khusus #

Empat tipe ini sering membingungkan tapi masing-masing punya peran yang sangat berbeda dalam sistem tipe TypeScript.

any — Keluar dari Sistem Tipe #

any menonaktifkan seluruh type checking untuk variabel tersebut. Ia bisa menerima semua nilai dan bisa diperlakukan seolah memiliki properti apapun.

// ANTI-PATTERN: Menggunakan any sebagai solusi mudah
let data: any = ambilDariAPI();
data.apapun.yang.kamu.mau; // Tidak ada error kompilasi — tapi bisa crash di runtime!

// KAPAN any masih diizinkan:
// 1. Migrasi bertahap dari JavaScript (tambahkan @ts-ignore atau @ts-nocheck)
// 2. Type declaration yang sangat dinamis dan benar-benar tidak bisa ditipe
// 3. Dalam blok yang sudah punya runtime validation sebelumnya

unknown — Tipe Aman untuk Nilai Tak Diketahui #

unknown menerima semua nilai seperti any, tapi TypeScript memaksa kamu melakukan type narrowing sebelum bisa menggunakannya. Ini adalah pengganti any yang aman.

// BENAR: Gunakan unknown untuk data eksternal yang belum divalidasi
function prosesInputEksternal(input: unknown): string {
  // Tidak bisa langsung pakai — harus narrow dulu
  if (typeof input === "string") {
    return input.trim(); // ✓ TypeScript tahu ini string
  }
  if (typeof input === "number") {
    return input.toFixed(2); // ✓ TypeScript tahu ini number
  }
  if (Array.isArray(input)) {
    return input.join(", "); // ✓ TypeScript tahu ini array
  }
  return String(input); // Fallback aman
}

never — Nilai yang Mustahil Ada #

never merepresentasikan nilai yang tidak pernah bisa ada — kondisi yang mustahil dicapai. Ia berguna untuk dua kasus utama:

// Kasus 1: Fungsi yang tidak pernah kembali normal (always throws atau infinite loop)
function panickan(pesan: string): never {
  throw new Error(`Fatal: ${pesan}`);
}

function loopSelamanya(): never {
  while (true) {
    // Melakukan sesuatu selamanya
  }
}

// Kasus 2: Exhaustive check — memastikan semua kasus union sudah ditangani
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:
      // Jika kamu menambahkan tipe baru ke union Bentuk tapi lupa menanganinya di sini,
      // TypeScript akan melaporkan error di baris ini — sangat berguna!
      const _exhaustiveCheck: never = bentuk;
      throw new Error(`Bentuk tidak dikenal: ${bentuk}`);
  }
}

void — Tidak Ada Return Value #

void digunakan untuk return type fungsi yang tidak mengembalikan nilai yang bermakna. Fungsi void boleh mengembalikan undefined secara eksplisit, tapi tidak boleh mengembalikan tipe lain.

// Fungsi void — hanya menjalankan aksi, tidak mengembalikan nilai
function logAktivitas(pengguna: string, aksi: string): void {
  const waktu = new Date().toISOString();
  console.log(`[${waktu}] ${pengguna}: ${aksi}`);
  // Tidak ada return — atau return; saja
}

// ANTI-PATTERN: Mengira void dan undefined sama persis
function contohVoid(): void {
  return undefined; // ✓ Ini diizinkan
  // return 42;     // ✗ Error: Type 'number' is not assignable to type 'void'
}

// Perbedaan penting: callback void boleh mengabaikan return value
type Callback = () => void;
const cb: Callback = () => 42; // ✓ Nilai return diabaikan

Perbandingan Keempat Tipe Khusus #

TipeBisa MenerimaBisa Digunakan Tanpa NarrowingKapan Pakai
anySemua nilai✓ (tidak aman)Darurat, migrasi JS
unknownSemua nilai✗ (harus narrow)Data eksternal/API
neverTidak ada nilaiExhaustive check, throw
voidundefinedReturn type fungsi tanpa nilai

null dan undefined — Ketiadaan Nilai #

Dua nilai ini berbeda secara semantik: undefined berarti “belum diinisialisasi atau tidak ada”, sementara null berarti “secara sengaja tidak ada nilai”. Dengan strictNullChecks aktif, keduanya tidak bisa masuk ke tipe lain secara diam-diam.

// undefined — nilai default saat tidak diinisialisasi
let belumDiisi: undefined = undefined;
let hasilKosong: string | undefined;      // Belum diisi

// null — ketiadaan nilai yang disengaja
let tidakAda: null = null;
let penggunaDitemukan: { nama: string } | null = null; // Belum ditemukan

// Perbedaan semantik dalam praktik
interface Profil {
  foto: string | null;      // null = sengaja tidak ada foto
  bio: string | undefined;  // undefined = pengguna belum mengisi bio
}

Nullish Coalescing dan Optional Chaining #

interface Pengguna {
  nama: string;
  alamat?: {
    kota?: string;
    provinsi?: string;
  };
}

const pengguna: Pengguna = { nama: "Budi" };

// Optional chaining — aman mengakses properti bertingkat yang mungkin undefined
const kota = pengguna.alamat?.kota;           // undefined (bukan error)
const panjangKota = pengguna.alamat?.kota?.length; // undefined

// Nullish coalescing — fallback untuk null ATAU undefined (bukan 0 atau "")
const kotaTampil = pengguna.alamat?.kota ?? "Kota tidak diketahui";
const stok = 0;
const stokTampil = stok ?? 10; // 0 — karena 0 bukan null/undefined

Union Types — Tipe A atau Tipe B #

Union types memungkinkan sebuah nilai memiliki lebih dari satu tipe yang mungkin. Operator | membaca “atau”.

// Union tipe primitif
let id: number | string;
id = 123;     // ✓
id = "usr-1"; // ✓

// Union dengan null — pola yang sangat umum
type NilaiOpsional<T> = T | null | undefined;

// Discriminated union — pattern yang powerful untuk 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 tahu state.data ada
    case "error":
      return `Error: ${state.pesan}`;           // TypeScript tahu state.pesan ada
  }
}

Intersection Types — Tipe A dan Tipe B #

Intersection types menggabungkan beberapa tipe menjadi satu — nilai harus memenuhi semua tipe yang digabungkan. Operator & membaca “dan”.

interface PunyaNama {
  nama: string;
}

interface PunyaUsia {
  usia: number;
}

interface PunyaEmail {
  email: string;
}

// Intersection — harus punya semua properti dari semua tipe
type PenggunaPenuh = PunyaNama & PunyaUsia & PunyaEmail;

const pengguna: PenggunaPenuh = {
  nama: "Budi",
  usia: 25,
  email: "[email protected]",
  // Wajib memiliki semua properti — tidak bisa kurang
};

// Pola umum: menambahkan metadata ke tipe yang sudah ada
type DenganTimestamp<T> = T & {
  dibuatPada: Date;
  diperbarui: Date;
};

type ProdukDenganTimestamp = DenganTimestamp<{
  nama: string;
  harga: number;
}>;

Union vs Intersection — Analogi Visual #

Union (A | B)        Intersection (A & B)
─────────────        ────────────────────
  ┌───┬───┐            ┌───────────┐
  │ A │ B │            │     A     │
  │   │   │            │   ┌───┐  │
  └───┴───┘            │   │ A&B   │
Nilai bisa di A         │   │   │  │
ATAU di B               │   └───┘  │
                        │     B    │
                        └──────────┘
                     Nilai harus di A
                     DAN di B sekaligus

Ringkasan #

  • number merepresentasikan semua angka (integer dan float); gunakan bigint untuk integer melebihi Number.MAX_SAFE_INTEGER, dan hindari kalkulasi uang langsung dengan float — simpan dalam satuan terkecil (sen/poin).
  • string mendukung template literal yang sangat fleksibel; TypeScript juga mendukung template literal types di level tipe untuk membuat pattern tipe yang ekspresif.
  • boolean sederhana tapi waspadai perbedaan antara boolean dengan nilai truthy/falsy JavaScript — cek eksplisit (!== null, .length > 0) lebih aman dari cek falsy implisit.
  • array menyimpan elemen bertipe homogen; gunakan sintaks T[] untuk keterbacaan, Array<T> untuk kasus generic yang lebih kompleks.
  • tuple memberikan struktur kaku dengan tipe per posisi; sangat berguna untuk return value fungsi yang mengembalikan beberapa nilai bertipe berbeda.
  • any adalah bahaya — hindari sebisa mungkin; gunakan unknown sebagai gantinya untuk data yang tipenya tidak diketahui, karena unknown memaksamu melakukan narrowing sebelum operasi.
  • never digunakan untuk fungsi yang tidak pernah kembali dan untuk exhaustive check dalam switch/union — compiler akan mengingatkanmu jika ada kasus union yang belum ditangani.
  • void adalah return type fungsi tanpa nilai bermakna; berbeda dari undefined dalam konteks callback — fungsi bertipe () => void boleh mengembalikan nilai tapi nilainya diabaikan.
  • Union types (A | B) untuk nilai yang bisa berupa salah satu dari beberapa tipe; gunakan discriminated union dengan properti literal untuk membuat state machine yang type-safe.
  • Intersection types (A & B) untuk nilai yang harus memenuhi semua tipe sekaligus; berguna untuk komposisi interface dan pola “mixin”.

← Sebelumnya: Konstanta   Berikutnya: Operator →

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