Target: Pemula yang telah memahami TypeScript Dasar dan ingin menguasai Pemrograman Berorientasi Objek di TypeScript (Classes, Access Modifiers public/private/protected/readonly, Parameter Properties shorthand, Getters/Setters, Static Members, Abstract Classes, Multiple Interface Implementation), Generics Komprehensif (<T>, Generic Functions, Generic Interfaces, Generic Classes, Generic Constraints extends keyof, Default Type Parameters), dan Utility Types Inti (Partial, Required, Readonly, Record, Pick, Omit) menggunakan TypeScript 5.5+.
Versi: TypeScript 5.x
Prasyarat:TypeScript Dasar
Fokus modul pembelajaran ini: mental model OOP & Encapsulation → Class & Access Modifiers → Parameter Properties → Abstract Classes & Interfaces → Generics <T> mental model → Generic Functions & Classes → Generic Constraints (extends) → keyof lookup constraints → Default Type Parameters → Utility Types Inti (Partial, Required, Readonly, Record, Pick, Omit) → Constructor Signatures → mini project Enterprise Generic In-Memory Repository & Polymorphic Event Bus.
Cara Belajar
🟢 Fundamental→ wajib dipahami: Class, Access Modifiers (public/private/protected/readonly), Parameter Properties, dan Getters/Setters🟡 Lanjutan→ pelajari setelah Class lancar: Abstract Classes, Multiple Interfaces, Generics Dasar (<T>), Generic Constraints (extends), dan keyof constraints🔴 Advanced / Operasional→ penting untuk arsitektur library/framework: Utility Types Inti (Partial/Pick/Omit/Record), Generic Factory Signatures, dan Anti-Pattern avoidance
Mental model alur abstraksi Generics <T> di TypeScript:
FUNGSI / CLASS GENERIK function identity<T>(arg: T): T │ ▼ PEMANGGIL MENYEDIAKAN TIPE SPESIFIK ┌─────────────────────────┼─────────────────────────┐ ▼ (Dipanggil dgn number) ▼ (Dipanggil dgn string) ▼ (Dipanggil dgn User) identity<number>(42) identity<string>("Halo") identity<User>(userObj) │ │ │ ▼ ▼ ▼ Compiler Mengetahui Compiler Mengetahui Compiler Mengetahui Return bertipe: NUMBER Return bertipe: STRING Return bertipe: USER │ │ │ └─────────────────────────┴─────────────────────────┘ ▼ TYPE SAFETY 100% TERJAGA TANPA REDUNDANSI KODE! (Zero 'any')
Hafalan:
Access Modifiers → kata kunci (public, private, protected, readonly) pengatur hak akses properti/method classParameter Properties → shorthand deklarasi dan inisialisasi properti otomatis langsung di parameter constructorAbstract Class → kelas template dasar yang tidak dapat di-instansiasi langsung dan mewajibkan implementasi di subclassGenerics (<T>) → fitur pembuatan komponen fleksibel dan reusable yang mempertahankan tipe data asli pemanggilGeneric Constraint → pembatasan tipe generik menggunakan keyword extends (T extends HasId atau K extends keyof T)keyof T → operator pengambil kumpulan seluruh key string literal dari suatu tipe objek TPartial<T> → utility type yang mengubah seluruh properti tipe T menjadi opsional (?)Required<T> → utility type yang mengubah seluruh properti tipe T menjadi wajib (menghapus ?)Readonly<T> → utility type yang mengunci seluruh properti tipe T menjadi tidak bisa diubah (readonly)Record<K, T> → utility type untuk membuat struktur objek kamus key-value dengan key bertipe K dan value bertipe TPick<T, K> → utility type untuk memilih sebagian daftar properti K dari tipe objek TOmit<T, K> → utility type untuk membuang daftar properti K dari tipe objek T
1. 🟢 Pengenalan Object-Oriented Programming (OOP) di TypeScript & Keunggulan Dibanding JS Classes Biasa
Konsep
Pada JavaScript biasa (ES6), class tidak memiliki penegakan tipe data statis atau enkapsulasi yang ketat (semua properti defaultnya adalah publik dan rentan terhadap mutasi liar).
public (Default): Properti/method dapat diakses dari mana saja (dalam class, turunan, maupun instance luar).
private: Hanya dapat diakses di dalam class itu sendiri (instance luar dan class turunan dilarang mengakses).
protected: Dapat diakses di dalam class itu sendiri DAN kelas turunannya (Subclass), tetapi tidak bisa diakses dari instance luar.
readonly: Properti hanya bisa diisi nilainya di deklarasi awal atau di dalam constructor, setelah itu tidak bisa diubah (Immutable).
Contoh
class BankAccount { public readonly accountNumber: string private balance: number protected accountHolder: string constructor(accountNumber: string, initialBalance: number, holder: string) { this.accountNumber = accountNumber this.balance = initialBalance this.accountHolder = holder } public deposit(amount: number): void { if (amount <= 0) throw new Error("Jumlah setoran harus positif!") this.balance += amount } public getBalance(): number { return this.balance }}const myAcc = new BankAccount("ACC-123456", 1000000, "Budi Santoso")myAcc.deposit(500000)console.log(`Saldo: Rp ${myAcc.getBalance()}`)// myAcc.balance = 9999999 ❌ ERROR: Property 'balance' is private// myAcc.accountNumber = "NEW-ID" ❌ ERROR: Cannot assign to 'accountNumber' because it is read-only
Hafalan:
public → bebas diakses dari mana sajaprivate → khusus di dalam class sendiriprotected → di dalam class sendiri + subclass turunanreadonly → tidak dapat dimutasi setelah inisialisasi constructor
Menuliskan properti class dan menginisialisasinya di constructor secara tradisional sangat bertele-tele (Boilerplate).
Parameter Properties Shorthand:
Dengan menambahkan modifier akses (public, private, protected, atau readonly) langsung pada parameter constructor, TypeScript otomatis:
Mendeklarasikan properti class tersebut.
Melakukan assign this.propName = propName di background secara otomatis.
Contoh
// ✅ Cara Cerdas & Bersih (Parameter Properties):class Customer { constructor( public readonly id: string, public name: string, private email: string, protected tier: "REGULAR" | "VIP" = "REGULAR" ) { // Tidak perlu menulis this.id = id; this.name = name; dsb! } public getContactEmail(): string { return this.email }}const cust = new Customer("c-101", "Alimur", "alimur@dev.com")console.log(cust.name) // "Alimur"
Kelas cetak biru dasar yang TIDAK BISA di-instansiasi langsung (new AbstractClass() akan ditolak compiler).
Dapat memuat method konkret biasa dan Abstract Methods (abstract methodName(): type) yang wajib diimplementasikan oleh setiap subclass turunan.
Contoh
abstract class PaymentProcessor { constructor(public gatewayName: string) {} // Method konkret bersama: public logTransaction(amount: number): void { console.log(`[Audit] Transaksi Rp ${amount} via ${this.gatewayName}`) } // Abstract Method: Wajib di-coding di class anak public abstract processPayment(amount: number, accountId: string): boolean}class XenditProcessor extends PaymentProcessor { constructor() { super("Xendit Gateway") } public processPayment(amount: number, accountId: string): boolean { this.logTransaction(amount) console.log(`Memotong saldo akun ${accountId} via Xendit API...`) return true }}
Hafalan:
abstract class Base { abstract process(): void; } → template class yang mewajibkan subclass mengimplementasikan method
8. 🟡 Mengimplementasikan Multiple Interfaces pada Class
Konsep
Sebuah class di TypeScript dapat mengimplementasikan lebih dari satu interface sekaligus menggunakan keyword implements dipisahkan tanda koma.
Ini menjamin class mematuhi banyak kontrak arsitektur (Separation of Concerns).
Contoh
interface IIdentifiable { id: string}interface IAuditable { createdAt: Date updatedAt: Date logAudit(): void}class OrderEntity implements IIdentifiable, IAuditable { constructor( public id: string, public totalAmount: number, public createdAt: Date = new Date(), public updatedAt: Date = new Date() ) {} public logAudit(): void { console.log(`Entitas Order ${this.id} tercatat pada ${this.createdAt.toISOString()}`) }}
Hafalan:
class MyClass implements InterfaceA, InterfaceB { ... } → mematuhi banyak kontrak interface sekaligus
9. 🟡 Pengenalan Generics (<T>) & Mental Model Reusability
Konsep
Tanpa Generics, jika Anda ingin membuat fungsi penampung yang bisa menerima sembarang tipe:
Anda menggunakan any→Kehilangan Type Safety dan autocompletion!
Anda membuat 10 fungsi berbeda (wrapNumber, wrapString, wrapUser) →Redundansi kode masif!
Solusi: Generics (<T>):
<T> adalah Variabel Penampung Tipe Data (Type Parameter).
Tipe data baru dikunci saat fungsi/class tersebut dipanggil, sehingga Type Safety tetap 100% utuh.
Contoh
// Fungsi Generik Murni:function wrapValue<T>(val: T): { value: T; timestamp: Date } { return { value: val, timestamp: new Date() }}// 1. Dipanggil dengan string -> Return otomatis { value: string, timestamp: Date }const wrappedStr = wrapValue("Halo TypeScript")console.log(wrappedStr.value.toUpperCase()) // Autocompletion string aktif!// 2. Dipanggil dengan number -> Return otomatis { value: number, timestamp: Date }const wrappedNum = wrapValue(150000)console.log(wrappedNum.value.toFixed(2)) // Autocompletion number aktif!
Hafalan:
function fn<T>(arg: T): T → fungsi generik yang mengunci tipe return persis sesuai tipe argumen input
10. 🟡 Generic Functions & Multi-Type Parameters
Konsep
Kita dapat menggunakan lebih dari satu variabel generik (konvensi penamaan: T, U, V, K, E):
T : Type
U / V : Type ke-2 / ke-3
K : Key
E : Element / Error
Contoh
// Fungsi Memasangkan Dua Tipe Berbeda Menjadi Tuplefunction createPair<T, U>(first: T, second: U): [T, U] { return [first, second]}const pair1 = createPair("UserID", 101) // [string, number]const pair2 = createPair(true, { role: "ADMIN" }) // [boolean, { role: string }]
Hafalan:
function pair<T, U>(a: T, b: U): [T, U] → fungsi generik dengan multi-parameter tipe
11. 🟡 Generic Interfaces & Generic Type Aliases
Konsep
Mendefinisikan format respon API atau wrapper data standar yang dapat membungkus model payload apa saja.
Contoh
// 1. Generic Interface Respon API Standarinterface ApiResponse<TData> { statusCode: number success: boolean message: string data: TData}// 2. Generic Type Alias Result Monadtype Result<T, E = Error> = | { success: true; value: T } | { success: false; error: E }// Penggunaan Nyata:interface UserProfile { id: string; name: string }const response: ApiResponse<UserProfile> = { statusCode: 200, success: true, message: "Berhasil", data: { id: "u-1", name: "Budi" }}
Class yang dapat mengelola koleksi data atau logika internal untuk sembarang tipe data secara terisolasi dan type-safe.
Contoh
class GenericQueue<TItem> { private items: TItem[] = [] public enqueue(item: TItem): void { this.items.push(item) } public dequeue(): TItem | undefined { return this.items.shift() } public size(): number { return this.items.length }}// Antrean String:const stringQueue = new GenericQueue<string>()stringQueue.enqueue("Antrean-1")// stringQueue.enqueue(999) ❌ ERROR: Argument of type 'number' is not assignable to 'string'
Hafalan:
class DataStore<T> { private data: T[] = []; push(item: T) {} } → generic class type-safe
13. 🟡 Generic Constraints dengan Keyword extends
Konsep
Secara default, <T> dapat berupa tipe apa saja. Jika Anda ingin memastikan bahwa <T>wajib memiliki properti tertentu (misal: harus memiliki properti id: string):
Operator keyof T menghasilkan gabungan string literal dari seluruh nama properti pada tipe T.
Kombinasi <T, K extends keyof T>:
Memungkinkan kita membuat fungsi pengambil properti objek yang 100% type-safe (nama properti divalidasi compiler dan return value otomatis bertipe T[K]).
Contoh
function getProperty<T, K extends keyof T>(targetObject: T, propertyKey: K): T[K] { return targetObject[propertyKey]}const user = { id: "u-1", name: "Alimur", age: 25 }const userName = getProperty(user, "name") // Tipe otomatis: stringconst userAge = getProperty(user, "age") // Tipe otomatis: number// getProperty(user, "alamat") ❌ ERROR: Argument of type '"alamat"' is not assignable to '"id" | "name" | "age"'
Hafalan:
<T, K extends keyof T>(obj: T, key: K): T[K] → pengambil properti objek yang 100% type-safe
15. 🟡 Default Generic Type Parameters
Konsep
Sama seperti parameter fungsi biasa yang bisa memiliki nilai default, parameter tipe generik dapat memiliki tipe default jika pemanggil tidak menentukannya secara eksplisit.
Format: <T = DefaultType>
Contoh
interface HttpResponse<T = Record<string, unknown>> { status: number data: T}// 1. Menggunakan tipe default (Record<string, unknown>):const defaultRes: HttpResponse = { status: 200, data: { customField: "halo" }}// 2. Menimpa dengan tipe spesifik:const userRes: HttpResponse<{ name: string }> = { status: 200, data: { name: "Budi" }}
Hafalan:
<T = string> → menyediakan tipe default jika type parameter tidak didefinisikan secara manual
16. 🔴 Utility Types Inti 1: Partial<T> & Required<T>
Konsep
Partial<T>: Mengubah semua properti tipe T menjadi opsional (?). Sangat ideal untuk payload HTTP PATCH / Update DTO.
Required<T>: Menghapus seluruh tanda ? dan mengubah semua properti menjadi wajib.
Contoh
interface UserProfile { id: string name: string email: string bio?: string}// 1. Partial: Semua properti jadi opsionaltype UpdateUserDto = Partial<UserProfile>const updateData: UpdateUserDto = { name: "Budi Baru" } // ✅ Valid hanya kirim name// 2. Required: 'bio' yang tadinya opsional sekarang wajib diisi!type StrictUserProfile = Required<UserProfile>
Hafalan:
Partial<T> → membuat seluruh properti menjadi opsional (ideal untuk update)Required<T> → membuat seluruh properti menjadi wajib (menghapus tanda ?)
17. 🔴 Utility Types Inti 2: Readonly<T> & Record<K, T>
Konsep
Readonly<T>: Mengunci seluruh properti tipe T agar tidak dapat di-assign ulang nilainya.
Record<Keys, Type>: Membuat tipe objek kamus (Dictionary / Map) di mana key-nya bertipe Keys dan nilainya bertipe Type.
Contoh
interface Product { id: string price: number}// 1. Readonlytype ImmutableProduct = Readonly<Product>const p: ImmutableProduct = { id: "p1", price: 50000 }// p.price = 60000 ❌ ERROR: Cannot assign to 'price' because it is a read-only property// 2. Record: Kamus Role ke Daftar Hak Aksestype AppRole = "ADMIN" | "USER" | "GUEST"type RolePermissionsMap = Record<AppRole, string[]>const permissions: RolePermissionsMap = { ADMIN: ["CREATE", "READ", "UPDATE", "DELETE"], USER: ["READ", "UPDATE"], GUEST: ["READ"]}
Hafalan:
Readonly<T> → mengunci semua properti menjadi immutableRecord<K, T> → membuat dictionary objek bertipe key K dan value T
Pick<T, Keys>: Membuat tipe baru dengan hanya mengambil (Pick) properti tertentu dari tipe T.
Omit<T, Keys>: Membuat tipe baru dengan membuang (Omit) properti tertentu dari tipe T.
Contoh
interface DatabaseUser { id: string username: string email: string passwordHash: string createdAt: Date}// 1. Pick: Hanya ambil 'id', 'username', dan 'email' untuk respon API publiktype PublicUserDto = Pick<DatabaseUser, "id" | "username" | "email">// 2. Omit: Buang 'passwordHash' dari database recordtype SafeUserRecord = Omit<DatabaseUser, "passwordHash">
Hafalan:
Pick<T, 'k1' | 'k2'> → mengambil subset properti tertentu dari tipe TOmit<T, 'k1' | 'k2'> → membuang properti tertentu dari tipe T
19. 🔴 Generic Factory & Constructor Signatures
Konsep
Dalam pola Dependency Injection atau Factory Pattern, kita sering perlu mengoper Class Constructor sebagai parameter dan membuat instance baru secara dinamis.
Tipe Constructor Signature:
type Constructor<T> = new (...args: any[]) => T
Contoh
type ClassConstructor<T> = new (...args: any[]) => Tfunction instantiateService<T>(ServiceClass: ClassConstructor<T>): T { console.log(`[DI Factory] Membuat instance untuk ${ServiceClass.name}...`) return new ServiceClass()}class AuthService { public authenticate() { return "Token Valid" }}const authInstance = instantiateService(AuthService) // Tipe otomatis: AuthServiceconsole.log(authInstance.authenticate())
Hafalan:
new (...args: any[]) => T → tipe constructor generic untuk Factory Pattern dan Dependency Injection
20. 🔴 Best Practice & Anti-Pattern Penggunaan Generics
Konsep
Generics adalah alat yang sangat kuat, namun sering disalahgunakan (Over-engineering).
WARNING
Anti-Pattern 1: Generics yang Tidak Perlu (Single-Use Generic)
Anti-Pattern 2: Menggunakan Generic Tanpa Mengaitkannya ke Return Value atau State
Jika tipe parameter T hanya muncul satu kali di parameter dan tidak mempengaruhi return value atau class state, kemungkinan besar Anda tidak membutuhkan generics!
Hafalan:
Aturan Emas Generics → gunakan generics hanya jika terdapat hubungan korelasi antara 2 parameter atau antara parameter dan return type
Langkah 1: Kuasai Class Modern & Access Modifiers├── Gunakan Parameter Properties untuk menyingkat constructor└── Enkapsulasi data dengan private, protected, dan getters/setters │ ▼Langkah 2: Terapkan Abstract Class & Interface Contracts├── Bangun hierarki class yang rapi menggunakan abstract class└── Pastikan konsistensi arsitektur dengan implements interface │ ▼Langkah 3: Bangun Abstraksi Reusable via Generics (<T>)├── Tulis fungsi generik dan batasi dengan extends constraints└── Gunakan operator keyof T untuk membaca properti secara type-safe │ ▼Langkah 4: Kuasai Manipulasi Tipe dengan Utility Types├── Gunakan Pick & Omit untuk membuat DTO input/output yang bersih└── Lindungi mutasi state dengan Readonly dan bangun kamus dengan Record │ ▼Langkah 5: Siap Melangkah ke TypeScript Advanced (Mapped & Conditional Types)!
25. 🏗️ Mini Project: Production-Ready Enterprise Generic In-Memory Repository, Polymorphic Event Bus & Type-Safe Database Entity Service
Aplikasi enterprise TypeScript lengkap, modern, dan runnable yang mengintegrasikan: Abstract Base Service, Generic In-Memory Repository <T extends BaseEntity>, Polymorphic Event Bus, Pick/Omit DTOs, dan Record In-Memory Index Store.
// =========================================================================// 1. DOMAIN CONTRACTS & BASE ENTITIES// =========================================================================export interface BaseEntity { readonly id: string createdAt: Date updatedAt: Date}// Model Userexport interface UserEntity extends BaseEntity { name: string email: string role: "ADMIN" | "STAFF" | "CUSTOMER" balance: number}// DTOs menggunakan Utility Types:export type CreateUserDTO = Omit<UserEntity, "id" | "createdAt" | "updatedAt">export type UpdateUserDTO = Partial<Omit<UserEntity, "id" | "createdAt">>export type UserPublicSummary = Pick<UserEntity, "id" | "name" | "role">// =========================================================================// 2. GENERIC REPOSITORY INTERFACE & IMPLEMENTATION// =========================================================================export interface IRepository<T extends BaseEntity> { findById(id: string): T | undefined findAll(): T[] create(dto: Omit<T, "id" | "createdAt" | "updatedAt">): T update(id: string, dto: Partial<T>): T | undefined delete(id: string): boolean}export class InMemoryRepository<T extends BaseEntity> implements IRepository<T> { // In-Memory Storage menggunakan Record Dictionary private storage: Record<string, T> = {} public findById(id: string): T | undefined { return this.storage[id] } public findAll(): T[] { return Object.values(this.storage) } public create(dto: Omit<T, "id" | "createdAt" | "updatedAt">): T { const now = new Date() const newId = `id-${Math.random().toString(36).substr(2, 9)}` const newEntity = { ...dto, id: newId, createdAt: now, updatedAt: now } as T this.storage[newId] = newEntity return newEntity } public update(id: string, dto: Partial<T>): T | undefined { const existing = this.storage[id] if (!existing) return undefined const updatedEntity: T = { ...existing, ...dto, updatedAt: new Date() } this.storage[id] = updatedEntity return updatedEntity } public delete(id: string): boolean { if (!this.storage[id]) return false delete this.storage[id] return true }}// =========================================================================// 3. POLYMORPHIC GENERIC EVENT BUS// =========================================================================export type EventCallback<TData> = (payload: TData) => voidexport class GenericEventBus { private listeners: Record<string, Array<EventCallback<any>>> = {} public on<TPayload>(eventName: string, callback: EventCallback<TPayload>): void { if (!this.listeners[eventName]) { this.listeners[eventName] = [] } this.listeners[eventName].push(callback) } public emit<TPayload>(eventName: string, payload: TPayload): void { const eventCallbacks = this.listeners[eventName] if (eventCallbacks) { eventCallbacks.forEach(cb => cb(payload)) } }}// =========================================================================// 4. ABSTRACT BUSINESS SERVICE & USER SERVICE IMPLEMENTATION// =========================================================================export abstract class AbstractDomainService<T extends BaseEntity> { constructor( protected readonly repository: IRepository<T>, protected readonly eventBus: GenericEventBus ) {} public abstract validate(entity: Partial<T>): boolean}export class UserService extends AbstractDomainService<UserEntity> { constructor( repository: IRepository<UserEntity>, eventBus: GenericEventBus ) { super(repository, eventBus) } public override validate(data: Partial<UserEntity>): boolean { if (data.balance !== undefined && data.balance < 0) { throw new Error("Saldo user tidak boleh bernilai negatif!") } return true } public registerUser(dto: CreateUserDTO): UserPublicSummary { this.validate(dto) const user = this.repository.create(dto) // Emit Event Notifikasi this.eventBus.emit<UserEntity>("USER_REGISTERED", user) // Return hanya field publik (Pick) return { id: user.id, name: user.name, role: user.role } } public topUpBalance(userId: string, amount: number): UserEntity { const user = this.repository.findById(userId) if (!user) throw new Error("User tidak ditemukan!") const updated = this.repository.update(userId, { balance: user.balance + amount }) if (!updated) throw new Error("Gagal mengupdate saldo!") this.eventBus.emit<{ userId: string; newBalance: number }>("BALANCE_UPDATED", { userId, newBalance: updated.balance }) return updated }}// =========================================================================// 5. SIMULASI RUNNABLE// =========================================================================// Inisialisasi Event Bus & Repositoryconst eventBus = new GenericEventBus()const userRepo = new InMemoryRepository<UserEntity>()const userService = new UserService(userRepo, eventBus)// Pasang Listener EventeventBus.on<UserEntity>("USER_REGISTERED", (user) => { console.log(`🔔 [Event Bus] User Baru Terdaftar: ${user.name} (${user.email}) - ID: ${user.id}`)})eventBus.on<{ userId: string; newBalance: number }>("BALANCE_UPDATED", (event) => { console.log(`💰 [Event Bus] Saldo User ${event.userId} bertambah menjadi Rp ${event.newBalance.toLocaleString('id-ID')}`)})// Eksekusi Pendaftaran Userconst newUser = userService.registerUser({ name: "Alimur", email: "alimur@dev.com", role: "ADMIN", balance: 500000})console.log("Ringkasan User:", newUser)// Eksekusi Top Up SaldouserService.topUpBalance(newUser.id, 250000)
Hasil Output Eksekusi Terminal
🔔 [Event Bus] User Baru Terdaftar: Alimur (alimur@dev.com) - ID: id-k8j3x9z1aRingkasan User: { id: 'id-k8j3x9z1a', name: 'Alimur', role: 'ADMIN' }💰 [Event Bus] Saldo User id-k8j3x9z1a bertambah menjadi Rp 750.000