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:#fffnumber — 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 #
| Tipe | Bisa Menerima | Bisa Digunakan Tanpa Narrowing | Kapan Pakai |
|---|---|---|---|
any | Semua nilai | ✓ (tidak aman) | Darurat, migrasi JS |
unknown | Semua nilai | ✗ (harus narrow) | Data eksternal/API |
never | Tidak ada nilai | — | Exhaustive check, throw |
void | undefined | — | Return 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 #
numbermerepresentasikan semua angka (integer dan float); gunakanbigintuntuk integer melebihiNumber.MAX_SAFE_INTEGER, dan hindari kalkulasi uang langsung dengan float — simpan dalam satuan terkecil (sen/poin).stringmendukung template literal yang sangat fleksibel; TypeScript juga mendukung template literal types di level tipe untuk membuat pattern tipe yang ekspresif.booleansederhana tapi waspadai perbedaan antarabooleandengan nilai truthy/falsy JavaScript — cek eksplisit (!== null,.length > 0) lebih aman dari cek falsy implisit.arraymenyimpan elemen bertipe homogen; gunakan sintaksT[]untuk keterbacaan,Array<T>untuk kasus generic yang lebih kompleks.tuplememberikan struktur kaku dengan tipe per posisi; sangat berguna untuk return value fungsi yang mengembalikan beberapa nilai bertipe berbeda.anyadalah bahaya — hindari sebisa mungkin; gunakanunknownsebagai gantinya untuk data yang tipenya tidak diketahui, karenaunknownmemaksamu melakukan narrowing sebelum operasi.neverdigunakan untuk fungsi yang tidak pernah kembali dan untuk exhaustive check dalam switch/union — compiler akan mengingatkanmu jika ada kasus union yang belum ditangani.voidadalah return type fungsi tanpa nilai bermakna; berbeda dariundefineddalam konteks callback — fungsi bertipe() => voidboleh 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”.