1. Pengenalan Generics
Generics adalah salah satu fitur paling powerful di TypeScript. Generics memungkinkan Anda menulis kode yang reusable dan type-safe untuk berbagai tipe data, tanpa harus mengorbankan keamanan tipe.
Tanpa generics, Anda harus memilih antara menggunakan any (kehilangan type safety) atau menulis duplikasi kode untuk setiap tipe. Generics memecahkan masalah ini dengan membiarkan tipe menjadi parameter.
Masalah Tanpa Generics
// ❌ Masalah 1: Menggunakan any — hilang type safety
function identitasAny(value: any): any {
return value;
}
let hasil1 = identitasAny("hello"); // tipe: any (tidak tahu string)
hasil1.nonExistentMethod(); // Tidak ada error! 💀
// ❌ Masalah 2: Duplikasi kode untuk setiap tipe
function identitasString(value: string): string { return value; }
function identitasNumber(value: number): number { return value; }
function identitasBoolean(value: boolean): boolean { return value; }
// ✅ Solusi: Menggunakan Generics
function identitas<T>(value: T): T {
return value;
}
let str = identitas("hello"); // tipe: string (otomatis!)
let num = identitas(42); // tipe: number (otomatis!)
let bool = identitas(true); // tipe: boolean (otomatis!)
// Bisa juga eksplisit
let hasil = identitas<string>("TypeScript"); // tipe: string
┌──────────────────────────────────────────────────────┐
│ GENERICS: TYPE PARAMETER │
│ │
│ ┌─────────────────────────────────────────────┐ │
│ │ function identitas<T>(value: T): T │ │
│ │ ▲ ▲ │ │
│ │ │ │ │ │
│ │ Type Parameter Return │ │
│ └─────────────────────────────────────────────┘ │
│ │
│ identitas("hello") → T = string → return string │
│ identitas(42) → T = number → return number │
│ identitas(true) → T = boolean │
│ identitas({a: 1}) → T = {a: number} │
│ │
│ T berubah sesuai input, tapi tetap type-safe! ✅ │
└──────────────────────────────────────────────────────┘
2. Generic Functions
Generic functions memungkinkan Anda mendefinisikan fungsi yang bekerja dengan berbagai tipe data sambil tetap mempertahankan type safety.
Satu Type Parameter
// Generic function dengan satu type parameter
function buatArray<T>(item: T, jumlah: number): T[] {
return Array(jumlah).fill(item);
}
let angkaArray = buatArray(10, 5); // number[]
let stringArray = buatArray("hi", 3); // string[]
let objArray = buatArray({ nama: "Budi" }, 2); // {nama: string}[]
console.log(angkaArray); // [10, 10, 10, 10, 10]
console.log(stringArray); // ["hi", "hi", "hi"]
// Generic function dengan array
function elemenPertama<T>(arr: T[]): T | undefined {
return arr[0];
}
let pertama = elemenPertama([1, 2, 3]); // number | undefined
let pertamaStr = elemenPertama(["a", "b"]); // string | undefined
console.log(pertama); // 1
console.log(pertamaStr); // "a"
Multiple Type Parameters
// Dua type parameters
function gabung<T, U>(first: T, second: U): [T, U] {
return [first, second];
}
let pasangan1 = gabung("Budi", 25); // [string, number]
let pasangan2 = gabung(true, "aktif"); // [boolean, string]
console.log(pasangan1); // ["Budi", 25]
// Generic untuk transformasi
function proses<TInput, TOutput>(
data: TInput,
transformer: (input: TInput) => TOutput
): TOutput {
return transformer(data);
}
let hasilStr = proses(42, (n) => n.toString()); // string
let hasilBool = proses("hello", (s) => s.length > 0); // boolean
console.log(hasilStr); // "42"
console.log(hasilBool); // true
// Generic dengan lebih dari 2 parameter
function merge<T, U, V>(obj1: T, obj2: U, obj3: V): T & U & V {
return { ...obj1, ...obj2, ...obj3 };
}
const merged = merge(
{ nama: "Budi" },
{ umur: 25 },
{ kota: "Jakarta" }
);
// merged memiliki tipe: { nama: string } & { umur: number } & { kota: string }
console.log(merged.nama); // "Budi"
console.log(merged.umur); // 25
Generic Arrow Functions
// Generic arrow function
const identitas = <T>(value: T): T => value;
// Perhatikan: di .tsx file, gunakan extends syntax:
// const identitas = <T extends unknown>(value: T): T => value;
// Generic untuk filter
const filterArray = <T>(arr: T[], predicate: (item: T) => boolean): T[] => {
return arr.filter(predicate);
};
const angka = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
const genap = filterArray(angka, (n) => n % 2 === 0);
console.log(genap); // [2, 4, 6, 8, 10]
const kata = ["apel", "mangga", "jeruk", "anggur"];
const dimulaiA = filterArray(kata, (s) => s.startsWith("a"));
console.log(dimulaiA); // ["apel", "anggur"]
// Generic untuk map
const mapArray = <T, U>(arr: T[], fn: (item: T) => U): U[] => {
return arr.map(fn);
};
const panjang = mapArray(["hello", "world"], (s) => s.length);
console.log(panjang); // [5, 5]
3. Generic Classes
Generic classes memungkinkan Anda membuat class yang bisa bekerja dengan berbagai tipe data. Ini sangat berguna untuk membuat struktur data generik seperti stack, queue, atau repository.
Stack (LIFO) dengan Generics
// Generic Stack — Last In, First Out
class Stack<T> {
private items: T[] = [];
push(item: T): void {
this.items.push(item);
}
pop(): T | undefined {
return this.items.pop();
}
peek(): T | undefined {
return this.items[this.items.length - 1];
}
get size(): number {
return this.items.length;
}
isEmpty(): boolean {
return this.items.length === 0;
}
clear(): void {
this.items = [];
}
}
// Stack untuk angka
const angkaStack = new Stack<number>();
angkaStack.push(10);
angkaStack.push(20);
angkaStack.push(30);
console.log(angkaStack.pop()); // 30
console.log(angkaStack.peek()); // 20
console.log(angkaStack.size); // 2
// Stack untuk string
const stringStack = new Stack<string>();
stringStack.push("hello");
stringStack.push("world");
// ❌ Error: Argument of type 'number' is not assignable
// stringStack.push(123);
// Type inference — tidak perlu <string>
const inferredStack = new Stack();
inferredStack.push("otomatis string"); // TypeScript infer tipe
Queue (FIFO) dengan Generics
// Generic Queue — First In, First Out
class Queue<T> {
private items: T[] = [];
enqueue(item: T): void {
this.items.push(item);
}
dequeue(): T | undefined {
return this.items.shift();
}
front(): T | undefined {
return this.items[0];
}
get size(): number {
return this.items.length;
}
isEmpty(): boolean {
return this.items.length === 0;
}
}
// Queue untuk antrian pasien
interface Pasien {
nama: string;
keluhan: string;
}
const antrian = new Queue<Pasien>();
antrian.enqueue({ nama: "Budi", keluhan: "Demam" });
antrian.enqueue({ nama: "Sari", keluhan: "Batuk" });
antrian.enqueue({ nama: "Andi", keluhan: "Flu" });
console.log(antrian.front()?.nama); // "Budi"
console.log(antrian.dequeue()?.nama); // "Budi"
console.log(antrian.front()?.nama); // "Sari"
console.log(antrian.size); // 2
Pair & Triple dengan Generics
// Generic Pair — menyimpan dua nilai dengan tipe berbeda
class Pair<TFirst, TSecond> {
constructor(
public first: TFirst,
public second: TSecond
) {}
toArray(): [TFirst, TSecond] {
return [this.first, this.second];
}
toString(): string {
return `(${this.first}, ${this.second})`;
}
mapFirst<U>(fn: (value: TFirst) => U): Pair<U, TSecond> {
return new Pair(fn(this.first), this.second);
}
mapSecond<U>(fn: (value: TSecond) => U): Pair<TFirst, U> {
return new Pair(this.first, fn(this.second));
}
}
const pair1 = new Pair("Budi", 25);
console.log(pair1.toString()); // (Budi, 25)
const pair2 = pair1.mapFirst(s => s.toUpperCase());
console.log(pair2.toString()); // (BUDI, 25)
// Triple dengan 3 tipe berbeda
class Triple<T1, T2, T3> {
constructor(
public first: T1,
public second: T2,
public third: T3
) {}
toArray(): [T1, T2, T3] {
return [this.first, this.second, this.third];
}
}
const koordinat = new Triple(-6.2088, 106.8456, "Jakarta");
console.log(koordinat.toArray());
// [-6.2088, 106.8456, "Jakarta"]
4. Generic Interfaces
Interface juga bisa menggunakan generics, membuat kontrak yang fleksibel untuk berbagai tipe data.
// Generic interface untuk response API
interface ApiResponse<TData> {
success: boolean;
data: TData;
message: string;
timestamp: number;
}
// Menggunakan generic interface
interface User {
id: number;
nama: string;
email: string;
}
interface Produk {
id: number;
nama: string;
harga: number;
}
// Response dengan data User
const userResponse: ApiResponse<User> = {
success: true,
data: { id: 1, nama: "Budi", email: "budi@mail.com" },
message: "Berhasil",
timestamp: Date.now()
};
// Response dengan data Produk[]
const produkResponse: ApiResponse<Produk[]> = {
success: true,
data: [
{ id: 1, nama: "Laptop", harga: 15000000 },
{ id: 2, nama: "Mouse", harga: 150000 }
],
message: "Daftar produk",
timestamp: Date.now()
};
// Generic interface untuk CRUD operations
interface Repository<T> {
findById(id: number): T | null;
findAll(): T[];
create(item: Omit<T, 'id'>): T;
update(id: number, item: Partial<T>): T | null;
delete(id: number): boolean;
}
// Implementasi Repository
class UserRepository implements Repository<User> {
private users: User[] = [];
private nextId = 1;
findById(id: number): User | null {
return this.users.find(u => u.id === id) || null;
}
findAll(): User[] {
return [...this.users];
}
create(item: Omit<User, 'id'>): User {
const user: User = { id: this.nextId++, ...item };
this.users.push(user);
return user;
}
update(id: number, item: Partial<User>): User | null {
const user = this.findById(id);
if (!user) return null;
Object.assign(user, item);
return user;
}
delete(id: number): boolean {
const index = this.users.findIndex(u => u.id === id);
if (index === -1) return false;
this.users.splice(index, 1);
return true;
}
}
5. Generic Constraints
Constraints membatasi tipe yang bisa digunakan sebagai type parameter. Ini memastikan bahwa generic type memiliki properti atau method tertentu.
// ❌ Masalah: T bisa tipe apapun, termasuk yang tidak punya .length
// function panjang<T>(value: T): number {
// return value.length; // Error! Property 'length' does not exist on type 'T'
// }
// ✅ Constraint: T harus punya property length
interface HasLength {
length: number;
}
function panjang<T extends HasLength>(value: T): number {
return value.length;
}
console.log(panjang("hello")); // 5 ✅ (string punya length)
console.log(panjang([1, 2, 3])); // 3 ✅ (array punya length)
console.log(panjang({ length: 10 })); // 10 ✅ (object dengan length)
// console.log(panjang(123)); // ❌ Error: number tidak punya length
// Constraint dengan keyof
function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] {
return obj[key];
}
const user = { nama: "Budi", umur: 25, email: "budi@mail.com" };
console.log(getProperty(user, "nama")); // "Budi" ✅
console.log(getProperty(user, "umur")); // 25 ✅
// console.log(getProperty(user, "alamat")); // ❌ Error: 'alamat' is not in keyof user
// Constraint dengan interface
interface Comparable<T> {
compareTo(other: T): number;
}
class Angka implements Comparable<Angka> {
constructor(public value: number) {}
compareTo(other: Angka): number {
return this.value - other.value;
}
}
function getMax<T extends Comparable<T>>(a: T, b: T): T {
return a.compareTo(b) >= 0 ? a : b;
}
const a = new Angka(10);
const b = new Angka(20);
console.log(getMax(a, b).value); // 20
Multiple Constraints
// T harus memenuhi SEMUA constraint (intersection)
interface Printable {
print(): string;
}
interface Loggable {
log(): void;
}
function proses<T extends Printable & Loggable>(item: T): void {
console.log(item.print());
item.log();
}
// Class yang memenuhi kedua constraint
class LogEntry implements Printable, Loggable {
constructor(private message: string) {}
print(): string {
return `[LOG] ${this.message}`;
}
log(): void {
console.log(`Writing to file: ${this.message}`);
}
}
const entry = new LogEntry("User login berhasil");
proses(entry); // ✅ Memenuhi kedua constraint
// Constraint dengan constructor
function buatInstance<T>(Type: new (...args: any[]) => T): T {
return new Type();
}
class Config {
apiUrl = "https://api.example.com";
timeout = 5000;
}
const config = buatInstance(Config);
console.log(config.apiUrl); // "https://api.example.com"
6. Default Generic Types
Seperti parameter fungsi yang bisa punya default value, type parameter juga bisa punya default type.
// Default generic type
interface ApiResponse<TData = unknown> {
success: boolean;
data: TData;
message: string;
}
// Tanpa type parameter — data bertipe unknown
const res1: ApiResponse = {
success: true,
data: "bisa apapun",
message: "OK"
};
// Dengan type parameter eksplisit
const res2: ApiResponse<number[]> = {
success: true,
data: [1, 2, 3],
message: "OK"
};
// Multiple defaults
interface Config<THost = string, TPort = number, TSecure = boolean> {
host: THost;
port: TPort;
secure: TSecure;
}
// Menggunakan semua default
const config1: Config = {
host: "localhost",
port: 3000,
secure: true
};
// Override sebagian
interface CustomConfig extends Config<string, number, boolean> {
apiKey: string;
}
// Generic dengan default dan constraint
interface PaginatedResponse<T extends { id: number } = { id: number }> {
items: T[];
total: number;
page: number;
pageSize: number;
}
const pagination: PaginatedResponse<User> = {
items: [
{ id: 1, nama: "Budi", email: "budi@mail.com" },
{ id: 2, nama: "Sari", email: "sari@mail.com" }
],
total: 100,
page: 1,
pageSize: 10
};
7. Utility Types
TypeScript menyediakan berbagai utility types bawaan yang menggunakan generics untuk memanipulasi tipe. Ini sangat berguna dalam pengembangan sehari-hari.
Partial, Required, Readonly
interface User {
id: number;
nama: string;
email: string;
umur: number;
}
// Partial<T> — semua properti jadi opsional
function updateUser(user: User, updates: Partial<User>): User {
return { ...user, ...updates };
}
const budi: User = { id: 1, nama: "Budi", email: "budi@mail.com", umur: 25 };
const updated = updateUser(budi, { umur: 26, email: "budi.baru@mail.com" });
// Required<T> — semua properti jadi wajib
interface OptionalUser {
id?: number;
nama?: string;
email?: string;
}
function createUser(data: Required<OptionalUser>): User {
return { id: data.id, nama: data.nama, email: data.email, umur: 0 };
}
// Readonly<T> — semua properti jadi readonly
const config: Readonly<{ apiUrl: string; timeout: number }> = {
apiUrl: "https://api.example.com",
timeout: 5000
};
// ❌ Error: Cannot assign to 'apiUrl' (readonly)
// config.apiUrl = "https://other.com";
Pick, Omit, Record
// Pick<T, K> — pilih sebagian properti
type UserPreview = Pick<User, 'id' | 'nama'>
// { id: number; nama: string }
const preview: UserPreview = { id: 1, nama: "Budi" };
// Omit<T, K> — hapus sebagian properti
type UserWithoutEmail = Omit<User, 'email'>
// { id: number; nama: string; umur: number }
const noEmail: UserWithoutEmail = { id: 1, nama: "Budi", umur: 25 };
// Record<K, V> — buat tipe dengan key-value yang ditentukan
type Roles = "admin" | "user" | "guest";
type RolePermissions = Record<Roles, string[]>;
const permissions: RolePermissions = {
admin: ["read", "write", "delete"],
user: ["read", "write"],
guest: ["read"]
};
console.log(permissions.admin); // ["read", "write", "delete"]
Exclude, Extract, ReturnType
// Exclude<T, U> — hapus tipe dari union
type SemuaHari = "Senin" | "Selasa" | "Rabu" | "Kamis" | "Jumat" | "Sabtu" | "Minggu";
type HariKerja = Exclude<SemuaHari, "Sabtu" | "Minggu">
// "Senin" | "Selasa" | "Rabu" | "Kamis" | "Jumat"
// Extract<T, U> — ambil tipe yang cocok
type HariWeekend = Extract<SemuaHari, "Sabtu" | "Minggu">
// "Sabtu" | "Minggu"
// ReturnType<T> — ambil return type dari fungsi
function buatUser() {
return { id: 1, nama: "Budi", email: "budi@mail.com" };
}
type UserReturn = ReturnType<typeof buatUser>
// { id: number; nama: string; email: string }
// Parameters<T> — ambil parameter types dari fungsi
function sapa(nama: string, umur: number): string {
return `Halo ${nama}, umur ${umur}`;
}
type SapaParams = Parameters<typeof sapa>
// [string, number]
// Awaited<T> — unwrap Promise type
async function fetchData(): Promise<string> {
return "data";
}
type Data = Awaited<ReturnType<typeof fetchData>>
// string
NonNullable & Extract dengan Generics
// NonNullable<T> — hapus null dan undefined dari tipe
type MaybeString = string | null | undefined;
type SureString = NonNullable<MaybeString> // string
// Custom utility type: DeepPartial
type DeepPartial<T> = {
[P in keyof T]?: T[P] extends object ? DeepPartial<T[P]> : T[P];
};
interface AppConfig {
database: {
host: string;
port: number;
credentials: {
username: string;
password: string;
};
};
server: {
port: number;
cors: boolean;
};
}
// DeepPartial — semua nested properties jadi opsional
function mergeConfig(
defaultConfig: AppConfig,
overrides: DeepPartial<AppConfig>
): AppConfig {
return {
database: {
...defaultConfig.database,
...overrides.database,
credentials: {
...defaultConfig.database.credentials,
...overrides.database?.credentials
}
},
server: {
...defaultConfig.server,
...overrides.server
}
};
}
// Hanya override yang diperlukan
const customConfig = mergeConfig(
{ database: { host: "localhost", port: 5432, credentials: { username: "root", password: "" } },
server: { port: 3000, cors: true } },
{ database: { port: 3306, credentials: { password: "rahasia" } } }
);
8. Praktik: Repository Pattern dengan Generics
Mari kita gabungkan semua konsep generics untuk membuat Repository Pattern yang robust dan reusable.
// Base entity interface
interface BaseEntity {
id: number;
createdAt: Date;
updatedAt: Date;
}
// Generic Repository interface
interface IRepository<T extends BaseEntity> {
findById(id: number): Promise<T | null>;
findAll(filter?: Partial<T>): Promise<T[]>;
create(data: Omit<T, 'id' | 'createdAt' | 'updatedAt'>): Promise<T>;
update(id: number, data: Partial<T>): Promise<T | null>;
delete(id: number): Promise<boolean>;
count(filter?: Partial<T>): Promise<number>;
}
// Generic Repository implementation
class InMemoryRepository<T extends BaseEntity> implements IRepository<T> {
private items: T[] = [];
private nextId = 1;
async findById(id: number): Promise<T | null> {
return this.items.find(item => item.id === id) || null;
}
async findAll(filter?: Partial<T>): Promise<T[]> {
if (!filter) return [...this.items];
return this.items.filter(item => {
return Object.entries(filter).every(([key, value]) => {
return (item as any)[key] === value;
});
});
}
async create(data: Omit<T, 'id' | 'createdAt' | 'updatedAt'>): Promise<T> {
const now = new Date();
const item = {
...data,
id: this.nextId++,
createdAt: now,
updatedAt: now
} as T;
this.items.push(item);
return item;
}
async update(id: number, data: Partial<T>): Promise<T | null> {
const index = this.items.findIndex(item => item.id === id);
if (index === -1) return null;
this.items[index] = {
...this.items[index],
...data,
updatedAt: new Date()
};
return this.items[index];
}
async delete(id: number): Promise<boolean> {
const index = this.items.findIndex(item => item.id === id);
if (index === -1) return false;
this.items.splice(index, 1);
return true;
}
async count(filter?: Partial<T>): Promise<number> {
const items = await this.findAll(filter);
return items.length;
}
}
// === Menggunakan Repository ===
// Definisikan entity
interface Mahasiswa extends BaseEntity {
nama: string;
nim: string;
jurusan: string;
ipk: number;
}
// Buat repository untuk Mahasiswa
const mhsRepo = new InMemoryRepository<Mahasiswa>();
// CRUD operations
async function demo(): Promise<void> {
// Create
const budi = await mhsRepo.create({
nama: "Budi Santoso",
nim: "2024001",
jurusan: "Teknik Informatika",
ipk: 3.85
});
const sari = await mhsRepo.create({
nama: "Sari Dewi",
nim: "2024002",
jurusan: "Sistem Informasi",
ipk: 3.92
});
// Find All
const semua = await mhsRepo.findAll();
console.log(`Total mahasiswa: ${semua.length}`);
// Find by filter
const jurusanTI = await mhsRepo.findAll({ jurusan: "Teknik Informatika" });
console.log(`Mahasiswa TI: ${jurusanTI.length}`);
// Update
await mhsRepo.update(budi.id, { ipk: 3.90 });
// Count
const total = await mhsRepo.count();
console.log(`Total: ${total}`);
}
demo();
Generics adalah fondasi dari type-safe code reuse. Mulailah dari yang sederhana, lalu tingkatkan complexity seiring pengalaman. Hindari over-generic — gunakan generics hanya ketika Anda benar-benar perlu bekerja dengan multiple types.
9. Quiz: Uji Pemahamanmu!
Setelah membaca tutorial di atas, jawablah 5 pertanyaan berikut untuk menguji pemahamanmu tentang TypeScript Generics: