Memahami Interface dan Type di TypeScript: Panduan Lengkap untuk Developer Modern

Dunia pengembangan web modern sangat bergantung pada TypeScript untuk membangun aplikasi yang lebih robust, scalable, dan mudah dipelihara. TypeScript membawa sistem tipe statis ke JavaScript, yang membantu kita menangkap banyak bug sebelum kode berjalan di produksi. Namun, dua fitur inti yang seringkali membuat bingung developer, terutama yang baru mengenal TypeScript, adalah interface dan type.

Keduanya tampak serupa dan seringkali bisa saling menggantikan, tetapi memiliki perbedaan fundamental dan kasus penggunaan spesifik yang esensial untuk dipahami. Kesalahan dalam memilih antara interface dan type tidak hanya bisa menyebabkan kode yang kurang optimal, tapi juga menghambat kemampuan refactoring dan skalabilitas proyek. Artikel ini akan membahas secara mendalam perbedaan, kegunaan, dan kapan sebaiknya menggunakan interface atau type, dilengkapi dengan contoh praktis dan pengalaman nyata di lapangan.

Daftar Isi sembunyikan

Mengapa Kita Membutuhkan Sistem Tipe di JavaScript?

Sebelum masuk ke interface dan type, mari kita pahami mengapa TypeScript, dengan sistem tipenya, menjadi begitu penting. JavaScript, sebagai bahasa yang dinamis, memungkinkan variabel untuk menyimpan tipe data apa pun. Fleksibilitas ini memang bagus untuk prototipe cepat, tetapi di proyek skala besar, hal ini bisa menjadi pedang bermata dua.

Bayangkan Anda memiliki objek yang merepresentasikan seorang pengguna. Tanpa sistem tipe, Anda mungkin salah mengetik properti userName menjadi username, atau mengirimkan angka alih-alih string ke properti email. Kesalahan-kesalahan semacam ini baru akan terdeteksi saat runtime, yang sangat memakan waktu dan berpotensi menyebabkan bug di produksi.

TypeScript hadir untuk mengatasi masalah ini. Dengan mendefinisikan struktur data dan tipe variabel di awal, TypeScript memastikan konsistensi dan integritas data. Ini seperti memiliki cetak biru (blueprint) untuk semua bagian kode Anda, sehingga semua orang yang bekerja di proyek tahu persis data apa yang diharapkan dan bagaimana cara menggunakannya.

Memahami interface di TypeScript

interface adalah cara yang sangat kuat untuk mendefinisikan “kontrak” dalam kode Anda. Pada dasarnya, interface mendefinisikan bentuk sebuah objek. Ini menjelaskan properti apa yang harus dimiliki sebuah objek, serta tipe data dari setiap properti tersebut. interface tidak menghasilkan kode JavaScript apa pun saat dikompilasi; ia hanya ada selama proses pengecekan tipe di TypeScript.

Definisi dan Tujuan Interface

Tujuan utama interface adalah untuk:

  • Mendefinisikan bentuk objek.
  • Menerapkan kontrak untuk kelas (class).
  • Memfasilitasi deklarasi penggabungan (declaration merging).
  • Meningkatkan keterbacaan dan konsistensi kode di seluruh proyek.

Contoh Dasar Penggunaan Interface

Mari kita lihat contoh sederhana. Kita ingin mendefinisikan bentuk objek untuk seorang pengguna.


interface User {
id: number;
name: string;
email?: string; // Properti opsional
isActive: boolean;
}

Di sini, User adalah sebuah interface. Objek apa pun yang bertipe User harus memiliki properti id (bertipe number), name (bertipe string), dan isActive (bertipe boolean). Properti email bersifat opsional karena ada tanda tanya (?) setelah namanya.

Kita bisa menggunakannya seperti ini:


const user1: User = {
id: 1,
name: "John Doe",
email: "john.doe@example.com",
isActive: true,
};

const user2: User = {
id: 2,
name: "Jane Smith",
isActive: false,
// email tidak wajib ada di sini
};

// Ini akan menghasilkan error karena properti 'id' hilang
// const user3: User = {
// name: "Invalid User",
// isActive: true,
// };

Kelebihan Interface: Declaration Merging

Salah satu fitur paling unik dan powerful dari interface adalah “declaration merging”. Ini berarti Anda dapat mendefinisikan interface dengan nama yang sama lebih dari sekali, dan TypeScript akan secara otomatis menggabungkan semua definisi tersebut menjadi satu interface tunggal.

Ini sangat berguna dalam skenario di mana Anda ingin memperluas sebuah interface dari modul yang berbeda atau dari library pihak ketiga tanpa mengubah definisi aslinya. Misalnya, Anda mendefinisikan User di satu file, dan di file lain Anda ingin menambahkan properti baru tanpa memodifikasi file pertama:


// file: user.ts
interface User {
id: number;
name: string;
}

// file: user-roles.ts
interface User {
roles: string[];
}

// file: app.ts
const admin: User = {
id: 1,
name: "Admin",
roles: ["admin", "editor"], // 'roles' ditambahkan secara otomatis
};

Secara efektif, TypeScript melihat User sebagai:


interface User {
id: number;
name: string;
roles: string[];
}

Fitur ini tidak tersedia untuk type aliases, yang merupakan perbedaan krusial.

Kelebihan Interface: Implementasi Class

interface dapat digunakan untuk mendefinisikan kontrak yang harus dipenuhi oleh sebuah class. Ini berarti sebuah class dapat implement satu atau lebih interface, memastikan bahwa class tersebut memiliki properti dan metode yang didefinisikan dalam interface.


interface Greetable {
name: string;
greet(message: string): void;
}

class Person implements Greetable {
name: string;

constructor(name: string) {
this.name = name;
}

greet(message: string): void {
console.log(`${message}, my name is ${this.name}`);
}
}

const p = new Person("Alice");
p.greet("Hello"); // Output: Hello, my name is Alice

Jika Person tidak mengimplementasikan properti name atau metode greet, TypeScript akan memberikan error, memaksa developer untuk mematuhi kontrak. Ini adalah pilar penting untuk arsitektur berorientasi objek di TypeScript.

Kelebihan Interface: Extensibility dengan extends

interface dapat saling meng-extend satu sama lain. Ini memungkinkan Anda untuk membangun hierarki tipe yang lebih kompleks dan menggunakan kembali definisi tipe. Ketika sebuah interface meng-extend interface lain, ia mewarisi semua properti dan metode dari interface yang di-extend.


interface BaseEntity {
id: number;
createdAt: Date;
updatedAt: Date;
}

interface Product extends BaseEntity {
name: string;
price: number;
description?: string;
}

const laptop: Product = {
id: 101,
createdAt: new Date(),
updatedAt: new Date(),
name: "MacBook Pro",
price: 25000000,
description: "High-performance laptop",
};

Dalam contoh ini, Product secara otomatis memiliki properti id, createdAt, dan updatedAt dari BaseEntity, selain properti spesifiknya sendiri. Ini sangat efektif untuk mengurangi duplikasi kode dan menjaga konsistensi tipe.

Memahami type di TypeScript

type aliases (atau hanya type) adalah cara lain untuk memberi nama pada tipe. Seperti interface, type alias tidak membuat tipe baru melainkan hanya membuat nama baru untuk tipe yang sudah ada. Ini sangat fleksibel karena dapat memberi nama pada tipe apa pun, termasuk tipe primitif, union, intersection, tuples, dan bahkan tipe yang lebih kompleks.

Definisi dan Tujuan Type

Tujuan utama type adalah untuk:

  • Memberi nama pada tipe apa pun (primitif, objek, union, intersection, tuple).
  • Membuat kombinasi tipe yang kompleks menggunakan operator tipe (union, intersection).
  • Meningkatkan keterbacaan kode dengan memberi nama pada tipe yang kompleks.

Contoh Dasar Penggunaan Type

Kita bisa mendefinisikan bentuk objek pengguna dengan type seperti ini:


type UserType = {
id: number;
name: string;
email?: string;
isActive: boolean;
};

Penggunaannya serupa dengan interface:


const userA: UserType = {
id: 1,
name: "Alex",
isActive: true,
};

Pada pandangan pertama, type dan interface untuk definisi objek tampak sangat mirip. Namun, perbedaan muncul pada kemampuan lainnya.

Kelebihan Type: Union Types

Salah satu kekuatan terbesar type adalah kemampuannya untuk mendefinisikan “union types”. Union types memungkinkan sebuah variabel untuk memiliki salah satu dari beberapa tipe. Ini sangat berguna ketika Anda mengharapkan data bisa datang dalam beberapa bentuk berbeda.


type Status = "pending" | "success" | "error";

let orderStatus: Status = "pending";
orderStatus = "success";
// orderStatus = "failed"; // Ini akan menghasilkan error karena "failed" bukan bagian dari Status

Anda juga bisa membuat union dari tipe objek:


type AdminUser = {
role: "admin";
privileges: string[];
};

type GuestUser = {
role: "guest";
lastLogin: Date;
};

type AuthenticatedUser = AdminUser | GuestUser;

const currentUser: AuthenticatedUser = {
role: "admin",
privileges: ["manage_users", "manage_content"],
};

const visitor: AuthenticatedUser = {
role: "guest",
lastLogin: new Date(),
};

interface tidak bisa langsung digunakan untuk mendefinisikan union types dengan sintaks |.

Kelebihan Type: Intersection Types

Intersection types (&) memungkinkan Anda untuk menggabungkan beberapa tipe menjadi satu tipe baru yang memiliki semua properti dari semua tipe yang digabungkan. Ini seperti menggabungkan dua atau lebih objek menjadi satu objek besar.


type HasName = {
name: string;
};

type HasAge = {
age: number;
};

type PersonInfo = HasName & HasAge;

const person: PersonInfo = {
name: "Budi",
age: 30,
};

Anda juga bisa mencapai hal serupa dengan interface menggunakan extends, tetapi type memberikan fleksibilitas lebih untuk kombinasi tipe arbitrer.

Kelebihan Type: Type Aliases untuk Primitif dan Literals

Anda bisa menggunakan type untuk memberi nama pada tipe primitif atau literal. Ini sangat membantu untuk meningkatkan keterbacaan kode ketika Anda memiliki nilai-nilai literal yang berulang.


type UserId = string;
type HexColor = `#${string}`;
type EvenNumber = 2 | 4 | 6 | 8;

const myId: UserId = "abc-123";
const primaryColor: HexColor = "#FF0000";
// const secondaryColor: HexColor = "red"; // Error

interface tidak bisa digunakan untuk tujuan ini; ia hanya bisa mendefinisikan bentuk objek.

Kelebihan Type: Conditional Types, Mapped Types, Template Literal Types

type sangat kuat dalam skenario tipe yang lebih maju seperti:

  • Conditional Types: Memungkinkan Anda membuat tipe yang memilih di antara dua tipe berbeda berdasarkan kondisi.
  • Mapped Types: Memungkinkan Anda mengubah bentuk properti yang ada dalam sebuah tipe untuk membuat tipe baru.
  • Template Literal Types: Memungkinkan Anda membuat string literal types berdasarkan pola string.

Contoh singkat untuk Mapped Types:


type Readonly = {
readonly [P in keyof T]: T[P];
};

type UserData = {
name: string;
age: number;
};

type ReadonlyUser = Readonly;
// ReadonlyUser akan menjadi: { readonly name: string; readonly age: number; }

Fitur-fitur ini sangat penting untuk membangun abstraksi tipe yang kompleks dan generik di TypeScript, dan ini adalah area di mana type jauh lebih unggul dari interface.

Perbandingan Langsung: interface vs type

Meskipun seringkali tumpang tindih, ada perbedaan kunci yang menentukan kapan harus menggunakan yang satu di atas yang lain.

Declaration Merging

  • interface: Mendukung declaration merging. Anda bisa mendefinisikan interface dengan nama yang sama di beberapa tempat, dan TypeScript akan menggabungkannya. Ini sangat berguna untuk library atau extend modul.
  • type: Tidak mendukung declaration merging. Jika Anda mencoba mendefinisikan type alias dengan nama yang sama dua kali, TypeScript akan menghasilkan error.

Implementasi Class

  • interface: Dapat di-implement oleh class. Ini adalah cara standar untuk mendefinisikan kontrak yang harus dipenuhi oleh sebuah class.
  • type: Tidak dapat langsung di-implement oleh class. Anda tidak bisa menulis class MyClass implements MyType jika MyType adalah type alias.

Extensibility

  • interface: Dapat di-extend oleh interface lain menggunakan kata kunci extends.
  • type: Dapat di-extend (dalam arti menggabungkan) menggunakan intersection types (&).

Kemampuan Memberi Nama Tipe

  • interface: Hanya bisa memberi nama pada bentuk objek.
  • type: Bisa memberi nama pada tipe apa pun, termasuk tipe primitif, union, intersection, tuples, dan tipe yang lebih kompleks.

Sintaks

  • interface: Menggunakan kata kunci interface.
  • type: Menggunakan kata kunci type.

Kapan Menggunakan interface dan Kapan Menggunakan type?

Dalam praktiknya, banyak developer mengalami kebingungan di sini. Aturan praktis yang banyak digunakan di komunitas TypeScript dan sering saya terapkan di proyek adalah:

  1. Gunakan interface untuk mendefinisikan bentuk objek publik atau kontrak kelas.

    Jika Anda mendefinisikan bentuk objek yang akan sering dipertukarkan antar modul, atau Anda ingin sebuah kelas mematuhi kontrak tertentu, interface adalah pilihan yang lebih baik. Kemampuan declaration merging juga membuatnya ideal untuk library atau untuk memperluas deklarasi yang sudah ada.

  2. Gunakan type untuk semua hal lain.

    Ini mencakup:

    • Union Types (string | number)
    • Intersection Types (TypeA & TypeB)
    • Alias untuk tipe primitif (type ID = string;)
    • Tuple Types (type Coordinates = [number, number];)
    • Tipe yang lebih kompleks dan canggih seperti Conditional Types, Mapped Types, dan Template Literal Types.

Pendekatan ini membantu menjaga konsistensi dalam codebase dan memanfaatkan kekuatan masing-masing fitur. Ketika Anda perlu mendefinisikan struktur data untuk objek yang akan berinteraksi dengan dunia luar atau akan diimplementasikan oleh kelas, interface adalah pilihan yang logis. Ketika Anda berurusan dengan kombinasi tipe yang lebih dinamis atau memberi nama pada tipe non-objek, type akan menjadi alat yang tepat.

Pengalaman dan Pertimbangan Praktis

Dalam proyek skala kecil, perbedaan antara interface dan type mungkin tidak terlalu terasa. Namun, di proyek yang lebih besar dengan banyak developer dan kode base yang terus berkembang, keputusan ini memiliki dampak signifikan pada maintainability dan arsitektur.

Konsistensi adalah Kunci

Salah satu pelajaran terbesar yang saya dapat adalah pentingnya konsistensi. Setelah Anda memilih gaya, entah itu lebih condong ke interface atau type, pastikan seluruh tim mengikuti aturan tersebut. Ini membantu mencegah kebingungan dan membuat kode lebih mudah dibaca oleh siapa pun yang baru bergabung dengan proyek.

Memahami Deklarasi Global dan Library

Saat berinteraksi dengan library pihak ketiga, Anda akan sering melihat interface digunakan untuk “augment” atau memperluas tipe yang sudah ada. Misalnya, jika Anda ingin menambahkan properti kustom ke objek Request di Express.js, Anda akan menggunakan declaration merging pada interface Request yang sudah ada. Ini adalah skenario di mana type tidak akan bisa melakukannya.

Performance (Minimal)

Ada mitos bahwa salah satu lebih baik untuk performa kompilasi. Dalam sebagian besar kasus, perbedaan performa antara interface dan type selama kompilasi TypeScript sangat minimal dan tidak signifikan untuk dipertimbangkan. Keduanya di-erase pada saat kompilasi ke JavaScript, jadi tidak ada perbedaan performa runtime sama sekali.

Refactoring dan Skalabilitas

Dengan interface, kemampuan declaration merging dan implementasi class memberikan fleksibilitas saat refactoring. Anda bisa menambahkan properti atau metode ke interface yang sudah ada tanpa perlu mengubah semua tempat di mana interface itu didefinisikan secara langsung. Ini sangat membantu di proyek yang terus berevolusi.

Sementara itu, type sangat unggul dalam menciptakan abstraksi tipe yang sangat spesifik dan kompleks. Kemampuan untuk mengombinasikan tipe dengan union dan intersection, serta fitur-fitur seperti Mapped Types, memungkinkan developer untuk membangun sistem tipe yang sangat ekspresif dan kokoh, yang bisa menangani hampir semua skenario data.

Masalah yang Sering Terjadi

Banyak developer mengalami beberapa masalah umum saat berinteraksi dengan interface dan type. Memahami ini bisa menghemat banyak waktu debugging.

1. Salah Menggunakan `interface` untuk Union Types

Gejala: Anda mencoba mendefinisikan tipe yang bisa menjadi salah satu dari beberapa objek berbeda, tetapi menggunakan sintaks interface yang salah.

Penyebab: interface tidak mendukung sintaks | untuk union. Ini adalah fitur eksklusif type.

Solusi: Gunakan type alias dan operator union (|) untuk mendefinisikan tipe gabungan.


// Salah:
// interface Event {
// type: 'click' | 'hover';
// // ...
// }

// Benar:
type ClickEvent = { type: 'click', x: number, y: number };
type HoverEvent = { type: 'hover', targetId: string };
type UIEvent = ClickEvent | HoverEvent;

const myEvent: UIEvent = { type: 'click', x: 10, y: 20 };

2. Mencoba Mengimplementasikan `type` di Class

Gejala: Anda memiliki type alias yang mendefinisikan bentuk objek, dan mencoba menggunakannya dengan implements pada sebuah class.

Penyebab: Kata kunci implements hanya berfungsi dengan interface (atau class lain). type alias tidak dapat diimplementasikan oleh class.

Solusi: Jika Anda ingin sebuah class mematuhi kontrak tipe, definisikan kontrak tersebut sebagai interface.


// Salah:
// type Runnable = { run(): void; };
// class Task implements Runnable { /* ... */ } // Error

// Benar:
interface Runnable {
run(): void;
}
class Task implements Runnable {
run(): void {
console.log("Task is running.");
}
}

3. Kebingungan dengan Extends dan Intersection

Gejala: Tidak yakin kapan harus menggunakan extends dengan interface atau operator intersection (&) dengan type untuk menggabungkan tipe.

Penyebab: Keduanya bisa menggabungkan properti, tetapi konteks penggunaannya berbeda.

Solusi:

  • Gunakan extends untuk interface ketika Anda ingin membangun hierarki tipe objek yang lebih terstruktur dan memanfaatkan fitur seperti implementasi kelas atau deklarasi penggabungan.
  • Gunakan & (intersection) untuk type ketika Anda ingin menggabungkan properti dari tipe apa pun secara ad-hoc, termasuk tipe primitif atau union, dan tidak memerlukan fitur interface.


// Interface dengan extends
interface Named { name: string; }
interface Aged extends Named { age: number; }
const personData: Aged = { name: "Carol", age: 40 };

// Type dengan intersection
type UserDetails = { email: string; };
type UserProfile = Named & UserDetails; // Menggabungkan properti dari Named dan UserDetails
const userProfileData: UserProfile = { name: "David", email: "david@example.com" };

4. Declaration Merging yang Tidak Disengaja

Gejala: Anda mendefinisikan dua interface dengan nama yang sama di file yang berbeda, tetapi TypeScript tidak memberikan error, dan tipe objek Anda memiliki properti dari kedua definisi.

Penyebab: Ini adalah fitur bawaan interface (declaration merging), bukan bug. Jika tidak diinginkan, ini bisa menyebabkan properti tak terduga.

Solusi: Jika Anda tidak ingin fitur declaration merging, pastikan nama interface Anda unik. Jika Anda memang sengaja ingin memperluas tipe global, fitur ini sangat membantu. Pahami kapan dan mengapa declaration merging terjadi.

FAQ

Bisakah type meng-implement interface?

Secara langsung, tidak. Anda tidak bisa menggunakan kata kunci implements dengan type alias pada sebuah class. implements hanya berfungsi untuk interface atau class. Namun, sebuah type alias bisa memiliki struktur yang sama persis dengan sebuah interface, dan TypeScript akan menganggapnya kompatibel (structural typing).

Bisakah interface meng-extend type?

Ya, sebuah interface dapat meng-extend sebuah type alias jika type alias tersebut mendefinisikan bentuk objek. Contohnya:


type Point = {
x: number;
y: number;
};
interface PointWithZ extends Point {
z: number;
}
const p3d: PointWithZ = { x: 1, y: 2, z: 3 };

Mana yang lebih baik untuk performance kompilasi TypeScript?

Dalam sebagian besar skenario, perbedaan performa antara interface dan type selama kompilasi sangat minimal dan tidak perlu menjadi pertimbangan utama. Fokuslah pada ekspresivitas dan kegunaan yang paling sesuai dengan kebutuhan Anda.

Apakah ada perbedaan saat bundling kode ke JavaScript?

Tidak. Baik interface maupun type adalah konstruksi khusus TypeScript yang di-erase sepenuhnya saat kode dikompilasi ke JavaScript. Tidak ada jejak mereka dalam output JavaScript akhir, sehingga tidak ada perbedaan performa atau ukuran bundle di runtime.

Kesimpulan

Memahami perbedaan dan kekuatan antara interface dan type adalah langkah penting untuk menjadi developer TypeScript yang mahir. Keduanya adalah fondasi dari sistem tipe TypeScript yang powerful, tetapi masing-masing memiliki area keunggulan spesifik.

Gunakan interface ketika Anda mendefinisikan bentuk objek yang akan diimplementasikan oleh kelas, atau ketika Anda ingin memanfaatkan declaration merging untuk memperluas definisi yang ada. Ini sangat cocok untuk membangun kontrak publik dan struktur data yang stabil.

Di sisi lain, type alias adalah alat yang sangat serbaguna untuk memberi nama pada tipe apa pun—mulai dari tipe primitif, union, intersection, hingga abstraksi tipe yang paling kompleks sekalipun. Ini memungkinkan Anda untuk membuat kombinasi tipe yang sangat ekspresif dan dinamis, yang sangat penting untuk logika bisnis yang kompleks dan generik.

Dengan pemahaman yang solid tentang kapan dan mengapa menggunakan masing-masing, Anda akan dapat menulis kode TypeScript yang lebih bersih, lebih aman, dan lebih mudah dipelihara, yang pada akhirnya akan meningkatkan produktivitas Anda dan kualitas aplikasi yang Anda bangun.

TAGS: TypeScript, Interface, Type, Coding, Developer Tools, Programming, Web Development, Software Engineering, Type System, JavaScript


Baca Juga

You May Also Like

Tinggalkan Balasan

Alamat email Anda tidak akan dipublikasikan. Ruas yang wajib ditandai *