Kenapa ini penting
Hampir semua data di aplikasi nyata berbentuk object: user, produk, pesanan, props komponen, respons API. Mendeskripsikan bentuk object dengan tepat adalah keterampilan TypeScript yang paling sering kamu pakai setiap hari. Begitu bentuknya tertulis, editor tahu isi object itu di mana pun ia dipakai, dan TypeScript menjaga agar tidak ada yang lupa mengisi properti wajib atau menulis properti yang tidak ada.
Anggap tipe object seperti formulir pendaftaran di kelurahan. Ada kolom wajib (nama, NIK), kolom opsional (email), dan kolom yang tidak boleh diubah setelah dicetak (nomor registrasi). Petugas menolak formulir yang kolom wajibnya kosong. Interface dan type alias adalah "formulir" itu dalam kode.
Konsep inti
Menulis tipe object
Bisa langsung (inline) atau diberi nama. Ada dua cara memberi nama: type dan interface.
// Type alias
type Produk = {
id: number;
nama: string;
harga: number;
};
// Interface
interface Pelanggan {
id: number;
nama: string;
email: string;
}
Untuk kasus sehari-hari keduanya hampir sama. Kita bahas bedanya di akhir bagian ini.
Properti opsional dan readonly
interface Pesanan {
readonly id: string; // tidak bisa diubah setelah dibuat
pelangganId: number;
catatan?: string; // opsional: boleh tidak ada
dibuatPada: Date;
}
catatan?: stringberarti properti boleh tidak ditulis. Saat dibaca, tipenyastring | undefined.readonlyhanya berlaku di compile time. Ia mencegah kamu menulispesanan.id = "baru", tapi tidak membekukan object di runtime (untuk itu adaObject.freeze).- Untuk array yang tidak boleh diubah:
readonly string[]atauReadonlyArray<string>.
Excess property check
Saat kamu menulis object literal langsung ke variabel bertipe, TypeScript memeriksa properti yang tidak dikenal. Ini yang menangkap salah ketik seperti hrga di modul pertama. Pemeriksaan ini sengaja hanya untuk object literal segar; object yang sudah tersimpan di variabel lain dinilai berdasarkan "punya properti yang dibutuhkan atau tidak" (structural typing).
Structural typing
TypeScript menilai tipe dari bentuknya, bukan dari namanya. Kalau sebuah object punya semua properti yang dibutuhkan Produk, ia dianggap Produk, meskipun tidak pernah dideklarasikan sebagai Produk. Ini berbeda dengan Java atau C#. Analoginya: satpam gedung tidak peduli kamu bekerja di perusahaan mana; ia hanya mengecek apakah kamu membawa kartu akses yang valid.
Index signature
Untuk object yang kuncinya dinamis, misalnya kamus terjemahan atau stok per kode barang:
type StokGudang = { [kodeBarang: string]: number };
const stok: StokGudang = { "BRG-01": 20, "BRG-02": 5 };
Di modul Utility Types kamu akan bertemu Record<string, number> yang artinya sama dan lebih ringkas.
Memperluas tipe: extends dan intersection &
interface Orang {
nama: string;
}
interface Karyawan extends Orang {
nip: string;
}
type Alamat = { kota: string };
type KaryawanLengkap = Karyawan & Alamat; // gabungan: nama + nip + kota
extends (untuk interface) dan & (intersection, untuk type) sama-sama menggabungkan properti. Bedanya, extends langsung melaporkan error kalau properti yang sama punya tipe bertabrakan, sedangkan & menghasilkan tipe yang mustahil dipenuhi (misalnya string & number menjadi never) dan error-nya baru terasa saat dipakai.
Interface vs type: kapan memakai yang mana
interface |
type |
|
|---|---|---|
| Object | Ya | Ya |
Union ("a" \| "b"), tuple, primitif, function |
Tidak | Ya |
| Memperluas | extends |
& |
| Declaration merging (dua deklarasi bernama sama digabung) | Ya | Tidak (error duplikat) |
| Pesan error dan performa compiler pada hierarki besar | Sedikit lebih baik | Baik |
Pedoman praktis yang dipakai banyak tim:
- Pakai
typesebagai default, terutama untuk union, tuple, dan tipe turunan (utility types). - Pakai
interfaceuntuk bentuk object yang akan diperluas (extends) atau API publik library, dan saat kamu memang butuh declaration merging (misalnya menambah properti keWindow). - Yang paling penting: konsisten dalam satu proyek.
Contoh kode
// toko.ts, memodelkan data toko online
interface Entitas {
readonly id: number;
readonly dibuatPada: Date;
}
interface Produk extends Entitas {
nama: string;
harga: number;
stok: number;
deskripsi?: string;
tag: readonly string[];
}
type Diskon = { persen: number; berlakuSampai: Date };
type ProdukPromo = Produk & { diskon: Diskon };
const kaos: Produk = {
id: 1,
dibuatPada: new Date("2026-10-01"),
nama: "Kaos Polos",
harga: 75000,
stok: 30,
tag: ["pakaian", "katun"],
};
const kaosPromo: ProdukPromo = {
...kaos,
diskon: { persen: 20, berlakuSampai: new Date("2026-10-31") },
};
function hargaAkhir(p: Produk | ProdukPromo): number {
if ("diskon" in p) return p.harga * (1 - p.diskon.persen / 100);
return p.harga;
}
console.log(hargaAkhir(kaos)); // → 75000
console.log(hargaAkhir(kaosPromo)); // → 60000
console.log(kaos.deskripsi ?? "(tanpa deskripsi)"); // → (tanpa deskripsi)
// Kesalahan yang ditangkap
kaos.id = 2; // ❌ error TS2540: Cannot assign to 'id' because it is a read-only property.
kaos.tag.push("baru"); // ❌ error TS2339: Property 'push' does not exist on type 'readonly string[]'.
const celana: Produk = { // ❌ error TS2741: Property 'stok' is missing in type '{ id: number; dibuatPada: Date; nama: string; harga: number; tag: never[]; }' but required in type 'Produk'.
id: 2,
dibuatPada: new Date(),
nama: "Celana Chino",
harga: 150000,
tag: [],
};
Structural typing dalam praktik:
// struktural.ts
type Titik = { x: number; y: number };
function jarakDariNol(t: Titik): number {
return Math.sqrt(t.x ** 2 + t.y ** 2);
}
// Object ini tidak pernah dideklarasikan sebagai Titik, tapi bentuknya cocok
const posisiKursor = { x: 3, y: 4, tombol: "kiri" };
console.log(jarakDariNol(posisiKursor)); // → 5 (properti ekstra boleh karena bukan literal segar)
console.log(jarakDariNol({ x: 3, y: 4, tombol: "kiri" })); // ❌ error TS2353: Object literal may only specify known properties, and 'tombol' does not exist in type 'Titik'.
// Declaration merging pada interface: menambah properti ke Window
declare global {
interface Window {
versiAplikasi: string;
}
}
window.versiAplikasi = "1.0.0";
console.log(window.versiAplikasi); // → 1.0.0
Kesalahan umum
- Menandai semua properti opsional "biar aman". Akibatnya setiap pemakaian harus memeriksa
undefined. Buat properti opsional hanya kalau memang boleh tidak ada. - Mengira
readonlymembekukan object di runtime.readonlyhanya pemeriksaan compile time. Untuk runtime gunakanObject.freeze(dan tipeReadonly<T>). - Bingung kenapa object dengan properti ekstra kadang lolos. Excess property check hanya untuk object literal yang langsung ditulis. Object dari variabel lain dinilai secara struktural.
- Mencampur
interfacedantypetanpa pola. Tidak salah secara teknis, tapi membingungkan tim. Sepakati satu pedoman. - Menduplikasi tipe yang mirip. Kalau
ProdukBarusama denganProduktanpaid, jangan ketik ulang semuanya. Turunkan dariProdukdenganOmit(modul Utility Types) atauextends.
Latihan
- Mudah, Buat
interface Bukudenganisbn(readonly),judul,penulis,tahun, dansinopsisopsional. Buat dua object buku. - Sedang, Buat
type Kontak = { email: string; telepon?: string }lalutype Siswa = { nama: string; kelas: string } & Kontak. Tulis functionkartuSiswa(s: Siswa): string. - Menantang, Buat
interface Kendaraan { roda: number; merek: string }, laluMobildanMotoryangextends Kendaraandengan properti khusus masing-masing (jumlahPintu,jenisMotor). Tulis functiondeskripsi(k: Mobil | Motor): stringyang memakai operatorinuntuk membedakan keduanya.
Lihat solusi
// 1. Mudah
interface Buku {
readonly isbn: string;
judul: string;
penulis: string;
tahun: number;
sinopsis?: string;
}
const buku1: Buku = { isbn: "978-602-1", judul: "Laskar Pelangi", penulis: "Andrea Hirata", tahun: 2005 };
const buku2: Buku = { isbn: "978-602-2", judul: "Bumi Manusia", penulis: "Pramoedya Ananta Toer", tahun: 1980, sinopsis: "Kisah Minke." };
console.log(buku1.judul, buku2.sinopsis); // → Laskar Pelangi Kisah Minke.
// 2. Sedang
type Kontak = { email: string; telepon?: string };
type Siswa = { nama: string; kelas: string } & Kontak;
function kartuSiswa(s: Siswa): string {
return `${s.nama} (${s.kelas}) - ${s.email}${s.telepon ? " / " + s.telepon : ""}`;
}
console.log(kartuSiswa({ nama: "Rina", kelas: "XI IPA 2", email: "rina@sekolah.id" }));
// → Rina (XI IPA 2) - rina@sekolah.id
// 3. Menantang
interface Kendaraan { roda: number; merek: string }
interface Mobil extends Kendaraan { jumlahPintu: number }
interface Motor extends Kendaraan { jenisMotor: "matic" | "bebek" | "sport" }
function deskripsi(k: Mobil | Motor): string {
if ("jumlahPintu" in k) return `Mobil ${k.merek}, ${k.jumlahPintu} pintu`;
return `Motor ${k.merek} jenis ${k.jenisMotor}`;
}
console.log(deskripsi({ roda: 4, merek: "Toyota", jumlahPintu: 5 })); // → Mobil Toyota, 5 pintu
console.log(deskripsi({ roda: 2, merek: "Honda", jenisMotor: "matic" })); // → Motor Honda jenis matic
Ringkasan
- Bentuk object ditulis dengan
typeatauinterface; keduanya hampir setara untuk object biasa. ?menandai properti opsional,readonlymencegah penulisan ulang di compile time.- TypeScript memakai structural typing: yang dinilai bentuknya, bukan namanya; object literal segar juga kena excess property check.
- Perluas tipe dengan
extends(interface) atau&(intersection). - Pakai
typeuntuk union/tuple/tipe turunan,interfaceuntuk object yang diperluas atau butuh declaration merging, dan konsisten.