Interface #

Interface adalah salah satu konstruksi paling penting dan paling sering digunakan di TypeScript. Ia mendefinisikan “bentuk” — struktur yang harus dimiliki sebuah nilai agar kompatibel dengan tipe tersebut. Berbeda dari kelas yang menghasilkan kode JavaScript, interface murni eksis di level TypeScript dan hilang sepenuhnya setelah kompilasi. Ini membuatnya sangat ringan: tidak ada overhead runtime, tidak ada kode tambahan di output, hanya informasi tipe yang digunakan compiler untuk validasi. Memahami interface berarti memahami bagaimana TypeScript mengimplementasikan structural typing — sistem tipe di mana kompatibilitas ditentukan berdasarkan bentuk, bukan nama atau asal-usul tipe.

Interface vs Type Alias — Kapan Memilih Mana? #

Sebelum membahas detail interface, penting memahami perbedaannya dengan type alias — karena keduanya sering bisa digunakan secara bergantian, dan banyak developer bingung harus memilih yang mana:

// Mendefinisikan bentuk object — bisa dilakukan keduanya
interface Pengguna {
  nama: string;
  email: string;
}

type PenggunaTipe = {
  nama: string;
  email: string;
};

// Keduanya bisa digunakan dengan cara yang sama
const p1: Pengguna = { nama: "Budi", email: "[email protected]" };
const p2: PenggunaTipe = { nama: "Siti", email: "[email protected]" };

Panduan praktis memilih antara keduanya:

SkenarioRekomendasiAlasan
Bentuk object / class contractinterfaceBisa di-extend dan di-implement, declaration merging
Union typetypeInterface tidak bisa berupa union
Intersection komplekstypeLebih ringkas dengan &
TupletypeInterface tidak bisa berupa tuple
Fungsi yang berdiri sendiritypeLebih ringkas
API publik libraryinterfaceDeclaration merging memudahkan augmentasi
// type HARUS digunakan untuk union — interface tidak bisa
type Status = "aktif" | "nonaktif" | "pending";
type IDFleksibel = string | number;

// type untuk tuple
type Koordinat = [number, number];
type Rentang = [min: number, max: number];

// interface lebih baik untuk kontrak object kompleks
interface KonfigurasiServer {
  host: string;
  port: number;
  ssl: boolean;
  timeout?: number;
}

Mendefinisikan Interface #

Interface didefinisikan dengan kata kunci interface. Semua properti yang dideklarasikan tanpa ? bersifat wajib — object harus memiliki semua properti tersebut dengan tipe yang sesuai:

interface Produk {
  id: string;
  nama: string;
  harga: number;
  kategori: string;
  stok: number;
}

// Object harus memenuhi semua properti wajib
const produk: Produk = {
  id: "PRD-001",
  nama: "Kurma Ajwa Premium",
  harga: 125_000,
  kategori: "makanan",
  stok: 50,
};

// ANTI-PATTERN: Properti berlebih — TypeScript menolak di assignment langsung
// const produkSalah: Produk = {
//   id: "PRD-002",
//   nama: "Madu",
//   harga: 75_000,
//   kategori: "minuman",
//   stok: 20,
//   diskon: 10, // ✗ Error: Object literal may only specify known properties
// };

// Tapi excess property check tidak berlaku saat assignment via variabel
const dataDariAPI = {
  id: "PRD-003",
  nama: "Minyak Zaitun",
  harga: 95_000,
  kategori: "dapur",
  stok: 15,
  diskon: 5, // Properti ekstra — tidak error saat lewat variabel
};
const produkDariAPI: Produk = dataDariAPI; // ✓ Tidak error

Structural Typing — Kompatibilitas Berdasarkan Bentuk #

TypeScript menggunakan structural typing: sebuah nilai kompatibel dengan interface jika ia memiliki setidaknya semua properti yang diwajibkan interface, terlepas dari asal-usulnya:

interface PunyaNama {
  nama: string;
}

// Fungsi yang menerima apapun yang punya properti 'nama'
function sapaPengguna(entitas: PunyaNama): string {
  return `Halo, ${entitas.nama}!`;
}

// Object literal biasa — kompatibel
sapaPengguna({ nama: "Budi" });

// Instance kelas — kompatibel jika punya properti 'nama'
class Mahasiswa {
  constructor(public nama: string, public nim: string) {}
}
sapaPengguna(new Mahasiswa("Siti", "20230001")); // ✓ Mahasiswa punya 'nama'

// Object dengan properti tambahan — tetap kompatibel
sapaPengguna({ nama: "Ahmad", jabatan: "Manager" }); // ✓ Punya 'nama', selebihnya diabaikan

Properti Opsional dan Readonly #

Properti Opsional dengan ? #

Properti opsional boleh ada atau tidak ada. Di dalam kode yang menggunakan properti opsional, TypeScript memaksa pengecekan sebelum penggunaan:

interface ProfilPengguna {
  nama: string;
  email: string;
  usia?: number;           // Opsional
  foto?: string | null;    // Opsional, bisa juga null eksplisit
  bio?: string;            // Opsional
  sosialMedia?: {
    twitter?: string;
    linkedin?: string;
    github?: string;
  };
}

function buatRingkasanProfil(profil: ProfilPengguna): string {
  // Harus narrow sebelum menggunakan properti opsional
  const infoUsia = profil.usia !== undefined ? `, ${profil.usia} tahun` : "";
  const infoBio  = profil.bio ? ` — "${profil.bio.slice(0, 50)}..."` : "";
  return `${profil.nama} <${profil.email}>${infoUsia}${infoBio}`;
}

// Object minimal — hanya properti wajib
const profilMinimal: ProfilPengguna = {
  nama: "Budi Santoso",
  email: "[email protected]",
};

// Object lengkap — semua properti diisi
const profilLengkap: ProfilPengguna = {
  nama: "Siti Rahma",
  email: "[email protected]",
  usia: 28,
  foto: "https://cdn.example.com/foto/siti.jpg",
  bio: "Software engineer yang gemar open source",
  sosialMedia: {
    github: "github.com/siti",
    linkedin: "linkedin.com/in/siti",
  },
};

Properti readonly #

readonly mencegah modifikasi properti setelah inisialisasi. Ini ditegakkan di level TypeScript saat kompilasi:

interface KonfigurasiImmutable {
  readonly databaseUrl: string;
  readonly secretKey: string;
  readonly environment: "development" | "staging" | "production";
  readonly batasCacheDetik: number;
}

const konfigurasi: KonfigurasiImmutable = {
  databaseUrl: "postgresql://localhost:5432/muslimapps",
  secretKey: process.env.SECRET_KEY ?? "dev-secret",
  environment: "production",
  batasCacheDetik: 3600,
};

// konfigurasi.databaseUrl = "url-lain"; // ✗ Error: read-only property
// konfigurasi.secretKey = "bocor";       // ✗ Error: read-only property

// Readonly hanya berlaku di level TypeScript — bukan runtime enforcement
// Untuk runtime, gunakan Object.freeze() seperti dibahas di artikel Konstanta

Interface untuk Fungsi dan Callable #

Interface bisa mendefinisikan bentuk fungsi — parameter apa yang diterima dan tipe apa yang dikembalikan:

// Interface untuk fungsi biasa
interface FungsiPerbandingan<T> {
  (a: T, b: T): number; // negatif, 0, atau positif
}

// Interface dengan metode opsional
interface Serializable {
  serialize(): string;
  deserialize?(data: string): void; // Metode opsional
}

// Penggunaan interface fungsi sebagai tipe parameter
function urutkan<T>(arr: T[], bandingkan: FungsiPerbandingan<T>): T[] {
  return [...arr].sort(bandingkan);
}

const angka = [5, 2, 8, 1, 9, 3];
const terurut = urutkan(angka, (a, b) => a - b);
// [1, 2, 3, 5, 8, 9]

const nama = ["Budi", "Ahmad", "Siti", "Dewi"];
const terurutAlfabet = urutkan(nama, (a, b) => a.localeCompare(b, "id"));
// ["Ahmad", "Budi", "Dewi", "Siti"]

Hybrid Type — Fungsi sekaligus Object #

Interface bisa mendefinisikan sesuatu yang bertindak sebagai fungsi sekaligus memiliki properti — berguna untuk library seperti jQuery atau Express:

interface FungsiDenganKonfigurasi {
  (input: string): string;      // Callable
  versi: string;                // Properti
  reset(): void;                // Metode
  konfigurasi: {
    caseSensitive: boolean;
    trim: boolean;
  };
}

function buatProsesor(): FungsiDenganKonfigurasi {
  const fn = function (input: string): string {
    const hasil = fn.konfigurasi.trim ? input.trim() : input;
    return fn.konfigurasi.caseSensitive ? hasil : hasil.toLowerCase();
  } as FungsiDenganKonfigurasi;

  fn.versi = "1.0.0";
  fn.konfigurasi = { caseSensitive: false, trim: true };
  fn.reset = () => {
    fn.konfigurasi = { caseSensitive: false, trim: true };
  };

  return fn;
}

const prosesor = buatProsesor();
console.log(prosesor("  Hello World  ")); // "hello world"
console.log(prosesor.versi);              // "1.0.0"

Index Signature — Properti Dinamis #

Index signature memungkinkan interface mendefinisikan objek dengan kunci yang tidak diketahui sebelumnya:

// Index signature dengan kunci string
interface KamusString {
  [kunci: string]: string;
}

const terjemahan: KamusString = {
  halo: "hello",
  terima_kasih: "thank you",
  selamat: "congratulations",
};

// Index signature dengan kunci number (untuk array-like)
interface DaftarBerindeks {
  [indeks: number]: string;
  length: number; // Properti tambahan yang kompatibel dengan nilai indeks
}

// Mencampur properti tertentu dengan index signature
interface KonfigurasiDinamis {
  host: string;        // Properti wajib yang diketahui
  port: number;        // Properti wajib yang diketahui
  [ekstra: string]: string | number; // Properti tambahan bebas
  // Semua properti spesifik harus kompatibel dengan tipe index signature
}

const konfig: KonfigurasiDinamis = {
  host: "localhost",
  port: 5432,
  namaDb: "muslimapps",    // Properti ekstra — diizinkan
  ssl: "true",              // Properti ekstra — harus string atau number
};
Index signature yang terlalu lebar ([kunci: string]: any) menghilangkan manfaat type safety. Lebih baik gunakan Record<string, NilaiSpesifik> atau definisikan interface yang lebih eksplisit. Gunakan index signature hanya jika kunci benar-benar dinamis dan tidak bisa diketahui sebelumnya.

Extends — Komposisi Interface #

Interface bisa mewarisi properti dari satu atau lebih interface lain menggunakan extends. Ini memungkinkan membangun hierarki tipe yang terstruktur:

// Interface dasar
interface Entitas {
  readonly id: string;
  dibuatPada: Date;
  diperbarui: Date;
}

interface PunyaNama {
  nama: string;
}

interface PunyaAlamat {
  jalan: string;
  kota: string;
  provinsi: string;
  kodePos: string;
}

// Extends dari satu interface
interface Pengguna extends Entitas {
  email: string;
  passwordHash: string;
  aktif: boolean;
}

// Extends dari beberapa interface sekaligus
interface Pelanggan extends Entitas, PunyaNama, PunyaAlamat {
  nomorPelanggan: string;
  levelLoyalitas: "bronze" | "silver" | "gold" | "platinum";
  totalBelanja: number;
}

// Interface anak bisa menambah properti baru dan mengoverride opsional menjadi wajib
interface PelangganVIP extends Pelanggan {
  manajerAkun: string;
  limitKredit: number;
}

// Penggunaan — harus memenuhi semua properti dari seluruh hierarki
const pelanggan: Pelanggan = {
  id: crypto.randomUUID(),
  dibuatPada: new Date(),
  diperbarui: new Date(),
  nama: "Budi Santoso",
  jalan: "Jl. Sudirman No. 1",
  kota: "Jakarta",
  provinsi: "DKI Jakarta",
  kodePos: "10220",
  nomorPelanggan: "PLG-001",
  levelLoyalitas: "gold",
  totalBelanja: 15_000_000,
};

Perbedaan extends Interface vs Intersection Type #

// Interface extends — nama tersendiri, bisa di-implement oleh class
interface HewanPeliharaan extends PunyaNama {
  jenis: string;
  usia: number;
}

// Intersection type — lebih fleksibel, bisa kombinasikan dengan apapun
type HewanPeliharaanType = PunyaNama & {
  jenis: string;
  usia: number;
};

// Keduanya menghasilkan bentuk yang sama, tapi:
// - Interface lebih baik untuk kelas yang implements
// - Intersection lebih baik untuk komposisi ad-hoc

Declaration Merging #

Fitur unik interface yang tidak dimiliki type alias adalah declaration merging — jika kamu mendeklarasikan interface dengan nama yang sama dua kali, TypeScript menggabungkannya secara otomatis:

// Deklarasi pertama
interface KonfigurasiServer {
  host: string;
  port: number;
}

// Deklarasi kedua — digabung dengan yang pertama
interface KonfigurasiServer {
  ssl: boolean;
  timeout: number;
}

// Hasil akhir: interface dengan keempat properti
const server: KonfigurasiServer = {
  host: "localhost",
  port: 3000,
  ssl: false,
  timeout: 30_000,
};

Declaration merging sangat berguna untuk augmentasi library pihak ketiga — kamu bisa menambahkan properti ke interface yang sudah ada di library tanpa mengubah source-nya:

// Contoh: menambahkan properti ke interface Request Express
// di file declarations/express.d.ts

declare global {
  namespace Express {
    interface Request {
      pengguna?: {
        id: string;
        peran: string;
      };
      requestId: string;
    }
  }
}

// Sekarang di seluruh aplikasi, req.pengguna dan req.requestId tersedia
// dengan type safety penuh

Interface sebagai Kontrak untuk Dependency Injection #

Salah satu penggunaan paling powerful dari interface adalah sebagai kontrak untuk dependency injection — memungkinkan swap implementasi tanpa mengubah kode yang bergantung padanya:

// Kontrak — mendefinisikan "apa" yang bisa dilakukan
interface RepositoriPengguna {
  ambilById(id: string): Promise<Pengguna | null>;
  simpan(pengguna: Pengguna): Promise<void>;
  hapus(id: string): Promise<boolean>;
  cariByEmail(email: string): Promise<Pengguna | null>;
}

interface LayananEmail {
  kirimSelamatDatang(email: string, nama: string): Promise<void>;
  kirimResetPassword(email: string, token: string): Promise<void>;
}

// Service bergantung pada kontrak, bukan implementasi konkret
class LayananPengguna {
  constructor(
    private readonly repo: RepositoriPengguna,
    private readonly email: LayananEmail
  ) {}

  async daftarPengguna(nama: string, emailBaru: string): Promise<Pengguna> {
    const sudahAda = await this.repo.cariByEmail(emailBaru);
    if (sudahAda) throw new Error("Email sudah terdaftar");

    const pengguna: Pengguna = {
      id: crypto.randomUUID(),
      nama,
      email: emailBaru,
      aktif: true,
      dibuatPada: new Date(),
      diperbarui: new Date(),
      passwordHash: "",
    };

    await this.repo.simpan(pengguna);
    await this.email.kirimSelamatDatang(emailBaru, nama);

    return pengguna;
  }
}

// Implementasi produksi
class RepositoriPenggunaMongoDB implements RepositoriPengguna {
  async ambilById(id: string): Promise<Pengguna | null> { /* MongoDB query */ return null; }
  async simpan(pengguna: Pengguna): Promise<void> { /* MongoDB insert */ }
  async hapus(id: string): Promise<boolean> { /* MongoDB delete */ return true; }
  async cariByEmail(email: string): Promise<Pengguna | null> { /* MongoDB query */ return null; }
}

// Implementasi untuk testing — mudah di-swap tanpa ubah LayananPengguna
class RepositoriPenggunaMemori implements RepositoriPengguna {
  private store = new Map<string, Pengguna>();

  async ambilById(id: string): Promise<Pengguna | null> {
    return this.store.get(id) ?? null;
  }
  async simpan(pengguna: Pengguna): Promise<void> {
    this.store.set(pengguna.id, pengguna);
  }
  async hapus(id: string): Promise<boolean> {
    return this.store.delete(id);
  }
  async cariByEmail(email: string): Promise<Pengguna | null> {
    for (const p of this.store.values()) {
      if (p.email === email) return p;
    }
    return null;
  }
}

Utility Types Berbasis Interface #

TypeScript menyediakan utility types bawaan yang bekerja dengan interface untuk menghasilkan varian baru:

interface Artikel {
  id: string;
  judul: string;
  konten: string;
  penulis: string;
  diterbitkan: boolean;
  dibuatPada: Date;
}

// Partial<T> — semua properti menjadi opsional (untuk update partial)
type PembaruanArtikel = Partial<Artikel>;
// { id?: string; judul?: string; konten?: string; ... }

function perbaruiArtikel(id: string, perubahan: PembaruanArtikel): void {
  console.log(`Memperbarui artikel ${id}:`, perubahan);
}
perbaruiArtikel("art-1", { judul: "Judul Baru" }); // Hanya ubah judul

// Required<T> — semua properti menjadi wajib (kebalikan Partial)
type ArtikelLengkap = Required<Artikel>;

// Pick<T, K> — ambil hanya properti tertentu
type RingkasanArtikel = Pick<Artikel, "id" | "judul" | "penulis">;
// { id: string; judul: string; penulis: string }

// Omit<T, K> — hapus properti tertentu
type ArtikelBaru = Omit<Artikel, "id" | "dibuatPada">;
// { judul: string; konten: string; penulis: string; diterbitkan: boolean }

// Readonly<T> — semua properti menjadi readonly
type ArtikelTerbit = Readonly<Artikel>;

// Record<K, V> — membuat tipe kamus
type IndeksArtikel = Record<string, Artikel>;

// Contoh penggunaan nyata: DTO untuk API
type CreateArtikelDTO = Omit<Artikel, "id" | "dibuatPada">;
type UpdateArtikelDTO = Partial<Omit<Artikel, "id" | "dibuatPada">>;
type ArtikelResponseDTO = Omit<Artikel, "konten">; // Tanpa konten penuh untuk daftar

Peta Hubungan Interface #

flowchart TD
    A[Interface] --> B[Mendefinisikan Bentuk]
    A --> C[Digunakan Sebagai]
    A --> D[Fitur Khusus]
    A --> E[Berinteraksi Dengan]

    B --> B1[Object shape]
    B --> B2[Tipe fungsi / callable]
    B --> B3[Index signature - kunci dinamis]
    B --> B4[Hybrid - fungsi + properti]

    C --> C1[Tipe variabel dan parameter]
    C --> C2[Return type fungsi]
    C --> C3[Generic constraint extends T]
    C --> C4[Kontrak dependency injection]

    D --> D1[Declaration merging]
    D --> D2[extends — warisan berganda]
    D --> D3[Structural typing — duck typing]

    E --> E1[Class implements]
    E --> E2[Type alias intersection &]
    E --> E3[Utility Types Partial Pick Omit]
    E --> E4[Augmentasi library global]

    style D1 fill:#cc5de8,color:#fff
    style D3 fill:#339af0,color:#fff
    style E1 fill:#51cf66,color:#fff
    style E3 fill:#fcc419,color:#000

Ringkasan #

  • Interface vs type alias — gunakan interface untuk bentuk object dan kontrak kelas karena mendukung extends, implements, dan declaration merging; gunakan type untuk union, tuple, dan komposisi kompleks yang tidak butuh augmentasi.
  • Structural typing adalah prinsip dasar — TypeScript tidak peduli nama tipe atau asal-usulnya, hanya bentuknya; sebuah objek kompatibel dengan interface jika ia memiliki semua properti yang diwajibkan.
  • Properti readonly mencegah modifikasi setelah inisialisasi di level kompilasi; gabungkan dengan Object.freeze() jika butuh perlindungan di runtime juga.
  • Index signature untuk objek dengan kunci dinamis; tapi jangan gunakan [key: string]: any — tentukan tipe nilai yang konkret untuk mempertahankan type safety.
  • extends berganda memungkinkan interface mewarisi dari beberapa interface sekaligus — ini cara TypeScript melakukan komposisi tipe tanpa multiple inheritance.
  • Declaration merging adalah fitur unik interface yang tidak ada di type alias — sangat berguna untuk augmentasi library pihak ketiga seperti menambahkan properti ke Request Express.
  • Interface sebagai kontrak dependency injection memungkinkan swap implementasi (produksi vs testing) tanpa mengubah kode yang bergantung — ini adalah pola fundamental untuk kode yang mudah diuji.
  • Utility types berbasis interface seperti Partial<T>, Required<T>, Pick<T, K>, dan Omit<T, K> sangat berguna untuk membuat varian dari interface yang sudah ada tanpa duplikasi definisi.

← Sebelumnya: Kelas   Berikutnya: Eksepsi →

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