Dalam pengembangan aplikasi modern, kita selalu mencari cara untuk membuat kode yang lebih efisien, reusable, dan mudah dirawat. TypeScript datang sebagai penyelamat dengan sistem tipenya yang kuat, membantu kita menangkap kesalahan lebih awal. Namun, ada kalanya kita ingin menulis fungsi, kelas, atau komponen yang bisa bekerja dengan berbagai jenis data tanpa kehilangan manfaat type safety.
Di sinilah Generic TypeScript berperan. Bayangkan Anda punya fungsi yang bekerja dengan angka, tapi di lain waktu Anda ingin fungsi yang sama bekerja dengan string, objek, atau bahkan tipe kustom. Apakah harus menulis ulang fungsi yang hampir sama berkali-kali? Tentu tidak. Generic memungkinkan kita membuat komponen yang fleksibel dan type-safe, seolah-olah kita ‘memparametrisasi’ tipe data itu sendiri. Ini bukan hanya tentang menghindari any, tapi juga tentang merancang arsitektur kode yang lebih kuat dan adaptif.
Apa Itu Generic di TypeScript?
Secara sederhana, Generic adalah cara untuk menulis kode yang bisa bekerja dengan berbagai tipe data sambil tetap mempertahankan informasi tipe tersebut. Anggap saja Generic seperti “variabel untuk tipe”. Sama seperti Anda membuat fungsi yang bisa menerima variabel numerik atau string, Generic memungkinkan Anda membuat fungsi atau struktur data yang bisa menerima “variabel tipe” — Tipe apa pun yang akan digunakan nanti.
Contoh paling dasar adalah fungsi identity. Fungsi ini menerima argumen dan mengembalikan argumen tersebut apa adanya. Tanpa Generic, Anda mungkin akan menulisnya seperti ini:
- Fungsi untuk angka:
function identityNumber(arg: number): number { return arg; } - Fungsi untuk string:
function identityString(arg: string): string { return arg; }
Atau yang lebih parah, Anda bisa menggunakan any:
function identityAny(arg: any): any { return arg; }
Masalah dengan any adalah kita kehilangan semua manfaat type checking yang ditawarkan TypeScript. Compiler tidak akan tahu tipe argumen yang masuk atau keluar, sehingga potensi kesalahan muncul di runtime.
Dengan Generic, kita bisa menulis satu fungsi identity yang type-safe untuk semua jenis data:
function identity<T>(arg: T): T {
return arg;
}
Di sini, <T> adalah deklarasi tipe Generic. T adalah placeholder untuk tipe apa pun yang akan kita gunakan nanti. Ketika kita memanggil fungsi ini:
let output1 = identity<string>("Hello"); // T akan menjadi string
let output2 = identity<number>(123); // T akan menjadi number
let output3 = identity(true); // TypeScript secara otomatis menginferensikan T sebagai boolean
TypeScript akan tahu bahwa output1 adalah string, output2 adalah number, dan output3 adalah boolean. Ini adalah kekuatan Generic: kode yang fleksibel tanpa mengorbankan keamanan tipe.
Kenapa Generic Penting untuk Developer Modern?
Sebagai developer yang bergelut dengan berbagai skala proyek, saya sering melihat betapa pentingnya Generic dalam membangun fondasi kode yang solid. Berikut adalah beberapa alasan mengapa Generic bukan sekadar fitur ‘canggih’, tapi sebuah keharusan:
1. Menghindari Bahaya ‘any’ dan Meningkatkan Type Safety
Seperti yang sudah kita bahas, penggunaan any adalah jalan pintas yang seringkali berakhir menjadi jebakan. Generic menawarkan alternatif yang lebih baik: kita mendapatkan fleksibilitas tanpa kehilangan informasi tipe. Dengan Generic, Anda dapat memastikan bahwa tipe input dan output konsisten, bahkan ketika Anda tidak tahu tipe spesifiknya saat menulis kode.
2. Membuat Kode Lebih Reusable dan Adaptif
Bayangkan Anda membuat komponen UI daftar (List) yang bisa menampilkan daftar pengguna, daftar produk, atau daftar komentar. Tanpa Generic, Anda mungkin perlu membuat tiga komponen terpisah atau menggunakan any. Dengan Generic, Anda bisa membuat satu komponen <List<T>> yang bisa menerima tipe data apa pun, dan TypeScript akan tetap melakukan type checking saat Anda menggunakannya.
3. Mengurangi Duplikasi Kode (DRY Principle)
Ketika Anda menyadari bahwa Anda menulis fungsi atau struktur data yang sama berulang kali hanya karena tipe datanya berbeda, itu adalah tanda bahwa Generic bisa menjadi solusi. Mengikuti prinsip Don’t Repeat Yourself (DRY) bukan hanya membuat kode lebih ringkas, tetapi juga lebih mudah di-maintain dan di-debug. Perubahan di satu tempat akan berlaku untuk semua penggunaan, daripada harus mengedit banyak file.
4. Meningkatkan Pengalaman Developer (DX)
Dengan Generic, editor kode Anda (seperti VS Code) bisa memberikan autocomplete dan validasi tipe yang akurat, bahkan untuk kode yang sangat generik. Ini berarti Anda akan menemukan kesalahan penulisan atau ketidakcocokan tipe saat Anda mengetik, bukan saat Anda menjalankan aplikasi. Produktivitas meningkat, frustrasi berkurang.
5. Pondasi untuk Library dan Framework
Jika Anda pernah menggunakan library seperti React, Redux, atau bahkan fitur bawaan TypeScript seperti Promise atau Array, Anda pasti sudah berinteraksi dengan Generic. Mereka adalah tulang punggung dari banyak API yang fleksibel dan kuat. Memahami Generic memungkinkan Anda untuk tidak hanya menggunakan library ini dengan lebih efektif, tetapi juga berkontribusi pada atau bahkan membangun library Anda sendiri dengan kualitas serupa.
Memahami Sintaks Dasar Generic
Mari kita ulas lagi sintaks dasar Generic dengan beberapa variasi.
Deklarasi Tipe Generic
Tipe Generic biasanya dinyatakan di dalam tanda kurung siku <> setelah nama fungsi atau kelas. Konvensi umum adalah menggunakan huruf kapital tunggal, seperti T (Type), U, V, K (Key), P (Prop), atau E (Element).
// Fungsi Generic
function identity<T>(arg: T): T {
return arg;
}
// Menggunakan fungsi identity
let myString = identity<string>("Hello TypeScript");
let myNumber = identity(123); // TypeScript menginferensikan T sebagai number
Dalam contoh di atas, T adalah parameter tipe. Saat Anda memanggil identity<string>("Hello TypeScript"), T secara efektif menjadi string di dalam lingkup fungsi tersebut.
Generic dengan Multiple Type Parameters
Anda bisa memiliki lebih dari satu parameter tipe Generic. Ini berguna ketika Anda perlu bekerja dengan beberapa tipe yang berbeda namun saling terkait.
function createPair<T, U>(first: T, second: U): [T, U] {
return [first, second];
}
let pair = createPair("nama", 100); // pair akan menjadi [string, number]
let anotherPair = createPair<boolean, string>(true, "status"); // anotherPair akan menjadi [boolean, string]
Di sini, T dan U adalah dua parameter tipe yang terpisah, memungkinkan fungsi untuk bekerja dengan dua tipe argumen yang berbeda dan mengembalikan tuple dengan tipe yang sesuai.
Generic Constraints: Membatasi Tipe Generic
Kadang-kadang, Anda ingin fungsi Generic Anda bekerja dengan semua jenis tipe, tetapi di lain waktu Anda mungkin perlu memastikan bahwa tipe tersebut memiliki properti tertentu. Misalnya, Anda mungkin ingin mencetak properti length dari argumen Generic. Jika Anda mencoba ini:
function logLength<T>(arg: T): T {
console.log(arg.length); // ERROR: Property 'length' does not exist on type 'T'.
return arg;
}
TypeScript akan memberikan error karena compiler tidak tahu apakah setiap tipe T akan memiliki properti length. Ini adalah masalah umum. Solusinya adalah menggunakan Generic Constraints dengan kata kunci extends.
Anda bisa memberi tahu TypeScript bahwa T harus memiliki properti length dengan cara ini:
interface Lengthy {
length: number;
}
function logLength<T extends Lengthy>(arg: T): T {
console.log(arg.length); // OK, karena T dijamin memiliki properti length
return arg;
}
logLength("Hello World"); // OK, string memiliki properti length
logLength([1, 2, 3]); // OK, array memiliki properti length
// logLength(10); // ERROR: Argument of type '10' is not assignable to parameter of type 'Lengthy'.
Dengan <T extends Lengthy>, kita memberi tahu TypeScript bahwa T harus berupa tipe yang mengimplementasikan interface Lengthy (yaitu, memiliki properti length bertipe number). Ini memungkinkan kita untuk menggunakan properti length dengan aman di dalam fungsi.
Constraint juga bisa berupa tipe primitif atau kombinasi lainnya:
function getProperty<T, K extends keyof T>(obj: T, key: K) {
return obj[key];
}
let user = { id: 1, name: "Alice" };
let id = getProperty(user, "id"); // id akan bertipe number
let name = getProperty(user, "name"); // name akan bertipe string
// let age = getProperty(user, "age"); // ERROR: Argument of type '"age"' is not assignable to parameter of type '"id" | "name"'.
Di sini, K extends keyof T memastikan bahwa argumen key harus berupa salah satu kunci yang ada di dalam objek T. Ini sangat kuat untuk bekerja dengan objek secara type-safe.
Generic di Interface dan Type Aliases
Generic tidak hanya terbatas pada fungsi. Anda bisa menggunakannya untuk membuat interface atau type alias yang lebih fleksibel.
Generic Interfaces
Misalnya, Anda ingin mendefinisikan sebuah struktur data ‘Kotak’ yang bisa menampung nilai dari tipe apa pun.
interface Box<T> {
value: T;
readonly createdAt: Date;
}
const stringBox: Box<string> = { value: "Hello", createdAt: new Date() };
const numberBox: Box<number> = { value: 123, createdAt: new Date() };
console.log(stringBox.value.toUpperCase()); // OK
// console.log(numberBox.value.toUpperCase()); // ERROR
Ini sangat berguna untuk mendefinisikan struktur data yang umum, seperti hasil dari panggilan API atau state di aplikasi, di mana isi datanya bisa bervariasi.
Generic Type Aliases
Mirip dengan interface, type alias juga bisa menggunakan Generic.
type Result<TData, TError> = {
status: "success";
data: TData;
} | {
status: "error";
error: TError;
};
type UserData = { id: number; name: string };
type FetchError = { code: number; message: string };
const successResult: Result<UserData, FetchError> = {
status: "success",
data: { id: 1, name: "Budi" }
};
const errorResult: Result<UserData, FetchError> = {
status: "error",
error: { code: 404, message: "User not found" }
};
Dengan Result<TData, TError>, kita membuat tipe yang sangat ekspresif untuk menangani hasil operasi yang bisa berhasil atau gagal, dengan tipe data spesifik untuk setiap skenario.
Generic di Kelas (Classes)
Kelas juga bisa menjadi Generic, yang sangat berguna untuk membuat struktur data yang bisa bekerja dengan tipe elemen yang berbeda, seperti tumpukan (Stack), antrian (Queue), atau pohon (Tree).
class Queue<T> {
private items: T[] = [];
enqueue(item: T): void {
this.items.push(item);
}
dequeue(): T | undefined {
return this.items.shift();
}
size(): number {
return this.items.length;
}
}
const stringQueue = new Queue<string>();
stringQueue.enqueue("Alice");
stringQueue.enqueue("Bob");
console.log(stringQueue.dequeue()); // "Alice"
const numberQueue = new Queue<number>();
numberQueue.enqueue(10);
numberQueue.enqueue(20);
// stringQueue.enqueue(30); // ERROR: Argument of type '30' is not assignable to parameter of type 'string'.
Dalam contoh ini, kelas Queue<T> didefinisikan untuk menampung elemen dari tipe T. Ketika kita membuat instance new Queue<string>(), semua metode di dalamnya akan tahu bahwa mereka harus bekerja dengan string. Ini memastikan konsistensi tipe di seluruh operasi antrian.
Studi Kasus Nyata: Menggunakan Generic di Lingkungan Web Development
Sebagai web developer, saya sering menemukan Generic sangat krusial dalam banyak skenario. Berikut beberapa contoh praktisnya:
1. Reusable API Fetcher
Anda sering mengambil data dari berbagai endpoint API, dan setiap endpoint mengembalikan struktur data yang berbeda. Dengan Generic, Anda bisa membuat fungsi fetcher yang universal.
interface ApiResponse<T> {
success: boolean;
data: T;
message?: string;
}
async function fetchData<T>(url: string): Promise<ApiResponse<T>> {
const response = await fetch(url);
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const result: ApiResponse<T> = await response.json();
return result;
}
interface User {
id: number;
name: string;
email: string;
}
interface Product {
id: number;
name: string;
price: number;
}
async function getUsers() {
try {
const data = await fetchData<User[]>("/api/users");
data.data.forEach(user => console.log(user.name)); // data.data adalah User[]
} catch (error) {
console.error(error);
}
}
async function getProducts() {
try {
const data = await fetchData<Product[]>("/api/products");
data.data.forEach(product => console.log(product.price)); // data.data adalah Product[]
} catch (error) {
console.error(error);
}
}
getUsers();
getProducts();
Di sini, fetchData<T> mengambil tipe data yang diharapkan dan mengembalikan Promise yang di-resolve dengan tipe ApiResponse<T> yang benar. Ini memastikan bahwa ketika Anda mendapatkan data pengguna, TypeScript tahu itu adalah User[], dan untuk produk, itu adalah Product[].
2. Generic Props untuk Komponen React
Saat membangun komponen UI yang bisa menampilkan berbagai jenis daftar atau item, Generic sangat membantu.
import React from 'react';
interface ListProps<T> {
items: T[];
renderItem: (item: T) => React.ReactNode;
}
function GenericList<T>({ items, renderItem }: ListProps<T>) {
return (
<ul>
{items.map((item, index) => (
<li key={index}>
{renderItem(item)}
</li>
))}
</ul>
);
}
// Penggunaan dengan daftar string
const names = ["Alice", "Bob", "Charlie"];
const NameList = () => (
<GenericList
items={names}
renderItem={(name) => <strong>{name}</strong>}
/>
);
// Penggunaan dengan daftar objek User
interface User {
id: number;
name: string;
}
const users: User[] = [
{ id: 1, name: "Dina" },
{ id: 2, name: "Eko" }
];
const UserInfoList = () => (
<GenericList
items={users}
renderItem={(user) => <span>ID: {user.id}, Name: {user.name}</span>}
/>
);
// <NameList />
// <UserInfoList />
Komponen GenericList<T> di sini menerima properti items dan renderItem, di mana T akan disesuaikan dengan tipe items. Ini membuat komponen sangat reusable.
3. Custom Hook untuk Local Storage
Saat membuat custom hook di React untuk berinteraksi dengan localStorage, Anda mungkin ingin menyimpan berbagai jenis data.
import { useState, useEffect } from 'react';
function useLocalStorage<T>(key: string, initialValue: T): [T, (value: T | ((val: T) => T)) => void] {
const [storedValue, setStoredValue] = useState<T>(() => {
try {
const item = window.localStorage.getItem(key);
return item ? JSON.parse(item) : initialValue;
} catch (error) {
console.error(error);
return initialValue;
}
});
const setValue = (value: T | ((val: T) => T)) => {
try {
const valueToStore = value instanceof Function ? value(storedValue) : value;
setStoredValue(valueToStore);
window.localStorage.setItem(key, JSON.stringify(valueToStore));
} catch (error) {
console.error(error);
}
};
return [storedValue, setValue];
}
// Penggunaan:
const MyComponent = () => {
const [name, setName] = useLocalStorage<string>("userName", "Guest");
const [age, setAge] = useLocalStorage<number>("userAge", 0);
const [isAdmin, setIsAdmin] = useLocalStorage<boolean>("isAdmin", false);
return (
<div>
<p>Name: {name}</p>
<input type="text" value={name} onChange={(e) => setName(e.target.value)} />
<p>Age: {age}</p>
<input type="number" value={age} onChange={(e) => setAge(Number(e.target.value))} />
</div>
);
};
Hook useLocalStorage<T> ini menggunakan Generic untuk memastikan bahwa nilai yang diambil dari localStorage dan dikembalikan oleh hook memiliki tipe yang tepat (T), memberikan keamanan tipe penuh untuk data yang disimpan secara persisten.
Pengalaman dan Pertimbangan Praktis
Meskipun Generic sangat powerful, ada beberapa hal yang perlu dipertimbangkan berdasarkan pengalaman saya di lapangan:
-
Kapan Menggunakan Generic? Gunakan Generic saat Anda ingin fungsi, kelas, atau interface Anda bekerja dengan berbagai tipe data tanpa kehilangan type safety. Indikator kuatnya adalah ketika Anda mulai berpikir untuk menggunakan
any, atau ketika Anda menulis ulang kode yang sama persis hanya karena tipe datanya berbeda. -
Hindari Over-Engineering: Jangan berlebihan menggunakan Generic di mana tidak diperlukan. Terkadang, tipe konkret lebih jelas dan mudah dibaca, terutama untuk pemula. Terlalu banyak Generic dengan batasan yang kompleks bisa membuat kode sulit dipahami dan di-debug. Tanyakan pada diri sendiri: “Apakah saya benar-benar butuh fleksibilitas tipe di sini, atau hanya satu tipe spesifik?”
-
Nama Parameter Tipe yang Deskriptif: Meskipun
Tadalah konvensi umum, untuk Generic yang lebih kompleks dengan banyak parameter, gunakan nama yang lebih deskriptif sepertiTItem,TKey,TValue, atauTState. Ini akan sangat membantu keterbacaan kode. -
Generic dan Inferensi Tipe: Manfaatkan kemampuan inferensi tipe otomatis TypeScript. Seringkali, Anda tidak perlu secara eksplisit menentukan tipe Generic (misalnya
identity<string>("Hello")), TypeScript bisa menebaknya dari argumen. Ini membuat kode lebih ringkas. -
Debugging Kode Generic: Saat terjadi kesalahan tipe dengan Generic, pesan error dari TypeScript mungkin terlihat menakutkan pada awalnya. Kuncinya adalah melacak di mana parameter tipe Generic (misalnya
T) didefinisikan dan bagaimana ia digunakan, serta memastikan semua batasan (extends) terpenuhi. -
Trade-off Fleksibilitas vs. Keterbacaan: Generic meningkatkan fleksibilitas dan reusability, tetapi terkadang dengan biaya keterbacaan, terutama untuk developer yang belum familiar. Pastikan tim Anda nyaman dengan penggunaan Generic jika Anda menggunakannya secara ekstensif dalam proyek.
Masalah yang Sering Terjadi
Selama proses belajar dan implementasi Generic, ada beberapa masalah umum yang sering saya temui, terutama bagi pemula:
1. “Property ‘X’ does not exist on type ‘T’.”
Ini adalah masalah paling umum. Anda mencoba mengakses properti atau metode pada variabel Generic T, tetapi TypeScript tidak bisa menjamin bahwa semua tipe yang diwakili oleh T akan memiliki properti atau metode tersebut.
Penyebab: Anda menggunakan tipe Generic tanpa batasan (constraint) yang cukup spesifik.
Solusi: Gunakan Generic Constraints (extends) untuk memberi tahu TypeScript bahwa T dijamin akan memiliki properti atau metode yang Anda butuhkan. Misalnya, jika Anda ingin mengakses .length, batasi T extends { length: number }.
2. Penggunaan ‘any’ karena Bingung dengan Generic
Ketika dihadapkan pada kompleksitas Generic atau batasan, godaan untuk kembali menggunakan any sangat besar. Ini akan menghilangkan semua manfaat type safety.
Penyebab: Kurangnya pemahaman tentang cara menerapkan Generic atau cara membuat batasan yang tepat.
Solusi: Luangkan waktu untuk memahami konsep dasar Generic dan constraints. Mulai dari contoh sederhana, lalu tingkatkan kompleksitasnya. Ingat, tujuan Generic adalah untuk menghindari any sambil tetap menjaga fleksibilitas.
3. “Type ‘T’ is not assignable to type ‘U’.” (Saat bekerja dengan multiple generics)
Anda mungkin mencoba mengembalikan atau menetapkan nilai dari satu parameter tipe Generic ke parameter tipe Generic lain tanpa ada jaminan kompatibilitas.
Penyebab: TypeScript tidak tahu apakah T dan U adalah tipe yang sama atau memiliki struktur yang kompatibel.
Solusi: Pastikan Anda menggunakan tipe Generic yang sesuai, atau tambahkan batasan yang relevan (misalnya, U extends T) jika Anda ingin memastikan bahwa U adalah subtipe dari T.
4. Tipe Generic yang Terlalu Luas atau Terlalu Spesifik
Terkadang, Anda mungkin membuat tipe Generic yang terlalu umum sehingga tidak memberikan banyak manfaat type checking, atau terlalu spesifik sehingga membatasi reusability.
Penyebab: Keseimbangan antara fleksibilitas dan keamanan tipe belum tercapai.
Solusi: Periksa kembali kebutuhan reusability Anda. Apakah Anda benar-benar butuh semua tipe di alam semesta, atau hanya subset tertentu? Sesuaikan batasan Generic Anda agar sesuai dengan kebutuhan fungsionalitas kode Anda.
FAQ
Apa bedanya Generic dengan ‘any’?
any mematikan semua pemeriksaan tipe, sehingga compiler TypeScript tidak akan memberikan error bahkan jika ada ketidaksesuaian tipe. Generic, di sisi lain, memungkinkan Anda untuk menulis kode yang fleksibel untuk berbagai tipe tetapi tetap mempertahankan semua pemeriksaan tipe yang kuat. Dengan Generic, Anda mendapatkan fleksibilitas dan keamanan tipe; dengan any, Anda hanya mendapatkan fleksibilitas tanpa keamanan.
Apakah Generic memperlambat runtime JavaScript?
Tidak. Generic adalah fitur compile-time only di TypeScript. Setelah kode TypeScript dikompilasi menjadi JavaScript, semua informasi tipe Generic akan hilang. Kode JavaScript yang dihasilkan tidak memiliki konsep Generic, sehingga tidak ada dampak pada performa runtime aplikasi Anda.
Kapan saya harus mulai menggunakan Generic?
Anda harus mulai menggunakan Generic ketika Anda menulis fungsi, kelas, atau interface yang dirancang untuk bekerja dengan berbagai tipe data, dan Anda ingin mempertahankan type safety. Indikator umumnya adalah saat Anda akan membuat duplikasi kode untuk setiap tipe data, atau Anda tergoda untuk menggunakan any untuk mencapai fleksibilitas.
Kesimpulan
Generic TypeScript adalah salah satu fitur paling kuat dan penting yang harus dikuasai oleh setiap developer modern. Fitur ini membuka pintu untuk menulis kode yang sangat reusable, fleksibel, dan yang terpenting, type-safe. Dengan memahami dan menerapkan Generic, Anda bisa membangun aplikasi yang lebih robust, lebih mudah dirawat, dan memberikan pengalaman pengembangan yang jauh lebih baik.
Mulai dari fungsi sederhana, lalu berlanjut ke interface, kelas, dan contoh-contoh di lingkungan web development, Generic akan menjadi alat andalan Anda. Ingat, kunci untuk menguasai Generic adalah praktik. Jangan takut untuk bereksperimen, membuat kesalahan, dan mempelajari batasan-batasannya. Semakin Anda menggunakannya, semakin natural dan intuitif rasanya.
TAGS: TypeScript, Generic, Type Safety, Coding, Programming, Developer Tools, JavaScript, Frontend Development, Software Engineering, Tutorial


