Ilustrasi flat vector dua bentuk geometris saling melengkapi sebagai simbol interface dan type alias

Selamat datang di episode ketiga Series TypeScript dari Nol. Di episode #104 kamu sudah kenalan dengan TypeScript dan alasan kenapa ia layak dipelajari, lalu di #105 kamu pegang tipe data dasarnya. Sekarang kita masuk ke bagian yang bikin TypeScript terasa seperti superpower: cara mendefinisikan bentuk data sendiri lewat interface dan type alias.

Kenapa topik ini penting? Karena di dunia nyata, datamu tidak pernah sekadar string atau number. Datamu berbentuk objek: user punya nama, email, dan tanggal lahir; produk punya harga, stok, dan kategori. interface dan type alias adalah alat untuk memberi nama pada bentuk-bentuk itu, supaya compiler bisa menjagamu dari salah ketik properti dan salah struktur data. Di episode berikut (#107) kita akan pakai konsep ini untuk bikin function yang fleksibel lewat generics, jadi pastikan fondasinya kuat di sini.

Cara Berpikirnya: Bentuk Data Itu Kontrak

Bayangkan kamu pesan martabak lewat ojek online. Kamu tidak menulis esai panjang, kamu mengisi form: jenis martabak (manis/telur), topping, level pedas. Form itu adalah kontrak: dapur tahu persis data apa yang akan datang, dan kamu tahu persis data apa yang harus dikirim. Kalau kamu mengisi "level pedas: biru", formnya menolak.

interface dan type alias adalah form untuk datamu. Bedanya dengan form kertas, kontrak ini diperiksa sebelum program jalan, oleh compiler. Salah isi satu huruf saja, langsung ditolak saat compile, bukan saat user sudah marah-marah karena aplikasinya crash.

Contoh paling dasar:

interface User {
  id: number;
  name: string;
  email: string;
}

const budi: User = {
  id: 1,
  name: "Budi",
  email: "budi@example.com"
};

Alur berpikirnya: kamu mendefinisikan bentuk dulu, baru datanya. Ini kebalikan dari JavaScript biasa yang "bikin objek dulu, urusan belakangan". Kenapa TypeScript memaksa pola ini? Karena objek yang bentuknya jelas jauh lebih gampang dipikirkan. Saat kamu baca function kirimEmail(user: User), kamu langsung tahu user itu seperti apa tanpa harus melacak dari mana objeknya berasal.

Berhenti dan tebak dulu! Apa yang terjadi kalau kode di bawah ini dikompilasi?

interface User {
  id: number;
  name: string;
}

const budi: User = {
  id: 1,
  name: "Budi",
  email: "budi@example.com"
};
Jawaban: error, properti email tidak dikenal di User. TypeScript tidak hanya memeriksa properti yang kurang, tapi juga properti yang berlebih pada object literal. Kenapa begitu ketat? Karena properti berlebih hampir selalu tanda ada salah paham: mungkin kamu mengira interfacenya punya field email padahal tidak, atau salah ketik emial. Compiler lebih baik cerewet di awal daripada membiarkan bug lolos diam-diam.

Dua Persepsi Salah yang Perlu Diluruskan

Mitos 1: "interface dan type itu sama saja, tinggal pilih yang disuka." Untuk bentuk objek sederhana memang terlihat identik, tapi keduanya punya kemampuan yang tidak saling menggantikan. interface bisa di-merge (dideklarasikan ulang untuk menambah properti) dan di-extends dengan gaya OOP. type bisa mendefinisikan union (string | number), tuple ([string, number]), dan tipe hasil komputasi yang interface tidak bisa. Memilih asal-asalan bikin kamu mentok di tengah jalan saat butuh fitur yang hanya dimiliki salah satunya.

Mitos 2: "Ngapain ribet bikin interface, kan bisa langsung pakai object literal." Bisa, untuk kode sekali pakai sepanjang sepuluh baris. Tapi begitu bentuk yang sama dipakai di lima function berbeda, kamu menduplikasi "formulir" itu lima kali. Sekali bentuknya berubah (misal user dapat field baru avatar), kamu harus berburu ke lima tempat. Dengan interface, perubahannya cukup di satu tempat. Ini bukan soal gaya, ini soal biaya perawatan kode.

Type Alias: Memberi Nama pada Tipe Apa Pun

type adalah cara memberi nama pada ekspresi tipe apa pun, tidak terbatas pada objek:

type ID = string | number;
type Status = "menunggu" | "diproses" | "selesai";
type Koordinat = [number, number];

Perhatikan Status. Ini namanya union type: sebuah variabel bertipe Status hanya boleh berisi salah satu dari tiga string itu. Kenapa ini berguna? Bandingkan dengan cara JavaScript biasa: status ditulis sebagai string bebas, dan suatu hari seseorang menulis "selesaii" (kelebihan satu huruf i). Tidak ada yang protes, bug baru ketahuan saat laporan status macet. Dengan union type, salah ketik langsung ditolak compiler.

Alur berpikirnya begini: setiap kali kamu punya sekumpulan nilai yang terbatas dan sudah diketahui (status order, role user, hari dalam seminggu), jadikan union type. Itu adalah cara termurah membeli keamanan tipe.

Berhenti dan tebak dulu! Kode mana yang lolos kompilasi?

type Status = "menunggu" | "diproses" | "selesai";

const a: Status = "menunggu";
const b: Status = "batal";
Jawaban: hanya a yang lolos; b error. "batal" bukan anggota union Status, jadi TypeScript menolaknya dengan pesan Type '"batal"' is not assignable to type 'Status'. Inilah gunanya union type: daftar nilai yang valid ditulis sekali, lalu compiler yang jadi penjaga. Kalau suatu hari status "batal" memang dibutuhkan, tambahkan ke unionnya, dan semua tempat yang memakai Status otomatis tahu.

Interface: Kontrak untuk Bentuk Objek

Kalau datamu berbentuk objek dan mungkin diperluas di kemudian hari, interface adalah pilihan natural:

interface Hewan {
  nama: string;
}

interface Anjing extends Hewan {
  ras: string;
}

const dogi: Anjing = { nama: "Dogi", ras: "Kintamani" };

Kata kunci extends di sini bukan hiasan. Ia menyatakan hubungan "Anjing adalah Hewan yang punya tambahan". Kenapa tidak pakai type Anjing = Hewan & { ras: string } saja? Hasilnya mirip, tapi extends mengkomunikasikan maksud dengan lebih jelas: ini pewarisan bentuk, bukan sekadar gabungan acak. Di codebase tim, kejelasan maksud itu berharga karena kode dibaca jauh lebih sering daripada ditulis.

Superpower khusus interface yang tidak dimiliki type adalah declaration merging: interface dengan nama sama yang dideklarasikan dua kali akan digabung otomatis.

interface Config {
  tema: string;
}

interface Config {
  bahasa: string;
}

const cfg: Config = { tema: "gelap", bahasa: "id" };

Kapan ini berguna di dunia nyata? Saat kamu memakai library pihak ketiga dan ingin menambah properti ke tipe bawaan library tanpa mengubah file librarynya. Pola ini dipakai luas di ekosistem TypeScript, misalnya menambah field custom ke tipe bawaan framework.

Aturan Praktis: Kapan Pakai yang Mana

Setelah paham bedanya, kamu butuh aturan yang bisa dipakai tanpa mikir lama. Ini panduan yang dipakai banyak tim:

  • Pakai interface untuk bentuk objek, terutama yang jadi kontrak publik: props component, model data, parameter API, atau bentuk yang akan di-extends class lain.
  • Pakai type untuk union, tuple, alias primitif, function signature, dan tipe hasil komputasi (mapped/conditional type).
  • Kalau keduanya bisa (bentuk objek sederhana), pilih satu dan konsisten satu proyek. Banyak style guide memilih interface untuk objek dan type untuk sisanya.

Kenapa aturan ini masuk akal? Karena ia memisahkan berdasarkan peran: interface untuk "bentuk benda", type untuk "nama bagi ekspresi tipe". Orang yang baca kodemu bisa menebak isinya hanya dari kata kuncinya. Konsistensi seperti ini yang membedakan kode amatir dan kode profesional.

Kuis cepat: Manakah pernyataan yang benar tentang interface dan type?

Latihan: Bukan Mengetik Ulang

Jangan cuma baca. Kerjakan ini di editor TypeScript-mu (pakai playground online kalau belum setup):

  1. Buat interface Produk dengan properti id: number, nama: string, harga: number, dan stok?: number (tanda tanya artinya opsional). Lalu buat dua objek: satu lengkap, satu tanpa stok. Pastikan keduanya lolos kompilasi.
  2. Ubah: buat type StatusProduk = "tersedia" | "habis" | "preorder", tambahkan properti status: StatusProduk ke interface-mu, lalu coba isi dengan string "diskon". Perhatikan errornya, lalu perbaiki dengan nilai yang valid.
  3. Tambah: buat interface ProdukDigital extends Produk yang menambah properti urlDownload: string. Buat satu objek ProdukDigital dan amati properti apa saja yang wajib diisi.
  4. Tantangan: deklarasikan interface Produk sekali lagi di file yang sama dengan tambahan properti kategori: string. Lihat apa yang terjadi pada objek-objek yang sudah kamu buat (declaration merging bekerja!).

Di episode berikutnya (#107) kita akan bikin function yang bisa bekerja untuk tipe apa pun tanpa kehilangan keamanan tipe, lewat generics. Interface dan type yang kamu kuasai hari ini adalah bahan bakarnya: generics tanpa bentuk data yang jelas sama saja bohong. Siapkan rasa penasaranmu, karena di sana ada satu trik yang bikin kamu berhenti menulis function yang sama berulang-ulang.