Masukkan Password
62 Membuat Kelas Generik
Di lecture ini kita masuk ke tahap lanjutan dari Generics, yaitu Generic Class.
Kalau sebelumnya kita belajar:
- Generic type
- Generic function
- Generic constraints
keyofT extends ...
sekarang kita akan melihat bagaimana class juga bisa dibuat generic.
1. Apa itu Generic Class?
Sederhananya:
Generic Class adalah class yang tidak menentukan tipe datanya dari awal, tetapi tipe tersebut diberikan ketika kita membuat object dari class tersebut.
Misalnya kita punya class ShoppingCart.
Kita ingin satu class tersebut bisa digunakan untuk:
- cart yang berisi
Book - cart yang berisi
Cloth - bahkan cart yang berisi
string - atau tipe lain.
Daripada membuat banyak class:
BookShoppingCart
ClothShoppingCart
StringShoppingCart
kita bisa membuat satu class generic:
class ShoppingCart<T> {
// ...
}
T di sini adalah placeholder untuk tipe data.
2. Mulai dari masalahnya
Misalkan kita punya dua tipe:
type Book = {
name: string;
pages: number;
price: number;
};
type Cloth = {
name: string;
size: string;
price: number;
};
Perhatikan bahwa Book dan Cloth berbeda.
Book
{
name: "Harry Potter",
pages: 300,
price: 20
}
punya:
name
pages
price
Sedangkan Cloth:
{
name: "T-Shirt",
size: "M",
price: 25
}
punya:
name
size
price
Tapi keduanya sama-sama ingin kita masukkan ke ShoppingCart.
3. Membuat ShoppingCart biasa
Awalnya kita mungkin membuat seperti ini:
class ShoppingCart {
private items = [];
addItem(item) {
this.items.push(item);
}
getItems() {
return this.items;
}
}
Secara konsep class ini sederhana:
ShoppingCart
│
├── items
│
├── addItem()
│
└── getItems()
Tetapi ada masalah.
items ini isinya apa?
Apakah:
Book[]
atau:
Cloth[]
atau:
string[]
TypeScript membutuhkan informasi tersebut supaya bisa memberikan type safety.
4. Solusinya: Generic Class
Kita tambahkan <T> setelah nama class:
class ShoppingCart<T> {
private items: T[] = [];
addItem(item: T) {
this.items.push(item);
}
getItems(): T[] {
return this.items;
}
}
Nah, sekarang T menjadi placeholder tipe data.
5. Memahami T dengan analogi
Bayangkan kita punya sebuah kotak:
┌─────────────────┐
│ ShoppingCart │
│ │
│ T[] │
└─────────────────┘
Kita belum menentukan isi kotaknya.
T artinya:
“Nanti isi tipe data saya ditentukan.”
Ketika kita membuat:
new ShoppingCart<Book>()
maka:
T = Book
Sehingga class tersebut secara konsep menjadi:
items: Book[]
dan:
addItem(item: Book)
serta:
getItems(): Book[]
6. Kalau menggunakan Cloth?
Misalnya:
new ShoppingCart<Cloth>()
maka:
T = Cloth
Sehingga:
items: Cloth[]
dan:
addItem(item: Cloth)
serta:
getItems(): Cloth[]
Jadi class-nya tetap satu, tetapi tipe datanya bisa berbeda.
7. Bedah kode Generic Class
Mari kita lihat lagi:
class ShoppingCart<T> {
private items: T[] = [];
addItem(item: T) {
this.items.push(item);
}
getItems(): T[] {
return this.items;
}
}
Kita pecah satu per satu.
class ShoppingCart<T>
class ShoppingCart<T>
Artinya:
Kita membuat class
ShoppingCartyang menerima sebuah type parameter bernamaT.
T belum ditentukan.
private items: T[]
private items: T[] = [];
Artinya:
itemsadalah array yang berisi data bertipeT.
Kalau:
T = Book
maka:
items: Book[]
Kalau:
T = Cloth
maka:
items: Cloth[]
addItem(item: T)
addItem(item: T) {
this.items.push(item);
}
Artinya:
Method
addItem()hanya menerima data yang sesuai dengan tipeT.
Kalau:
T = Book
maka:
addItem(item: Book)
Kalau:
T = Cloth
maka:
addItem(item: Cloth)
getItems(): T[]
getItems(): T[] {
return this.items;
}
Artinya:
Method ini mengembalikan array dengan tipe
T.
Kalau cart-nya Book, hasilnya:
Book[]
Kalau cart-nya Cloth, hasilnya:
Cloth[]
8. Menggunakan Generic Class dengan Book
Sekarang kita buat object:
const bookCart = new ShoppingCart<Book>();
Perhatikan:
<Book>
Di sini kita mengatakan:
“Untuk object
bookCart, gunakanBooksebagaiT.”
Jadi:
ShoppingCart<T>
↓
T = Book
Akibatnya class menjadi secara konsep:
items: Book[];
addItem(item: Book);
getItems(): Book[];
9. Menambahkan Book
Sekarang kita bisa:
bookCart.addItem({
name: "Harry Potter",
pages: 300,
price: 20
});
Ini valid karena object tersebut sesuai dengan Book.
Kita juga bisa menambahkan:
bookCart.addItem({
name: "Clean Code",
pages: 400,
price: 30
});
Kemudian:
const books = bookCart.getItems();
TypeScript tahu bahwa:
books
bertipe:
Book[]
Jadi kita tidak kehilangan informasi tipe.
10. Type safety-nya terlihat di sini
Misalnya kita melakukan:
bookCart.addItem({
name: "T-Shirt",
size: "M",
price: 25
});
TypeScript akan menolak.
Kenapa?
Karena:
bookCart
sudah ditentukan sebagai:
ShoppingCart<Book>
Jadi:
addItem()
hanya menerima:
Book
bukan:
Cloth
Ini salah satu keuntungan besar Generic.
11. Sekarang buat Cloth Cart
Kita bisa menggunakan class yang sama:
const clothCart = new ShoppingCart<Cloth>();
Sekarang:
ShoppingCart<T>
↓
T = Cloth
Sehingga secara konsep:
items: Cloth[];
addItem(item: Cloth);
getItems(): Cloth[];
Tambahkan Cloth
clothCart.addItem({
name: "T-Shirt",
size: "M",
price: 25
});
Valid.
Kemudian:
clothCart.addItem({
name: "Jacket",
size: "L",
price: 50
});
Juga valid.
Dan:
const clothes = clothCart.getItems();
TypeScript tahu:
clothes
bertipe:
Cloth[]
12. Satu class, banyak tipe
Ini inti utama lecture.
Kita hanya punya satu class:
class ShoppingCart<T> {
private items: T[] = [];
addItem(item: T) {
this.items.push(item);
}
getItems(): T[] {
return this.items;
}
}
Tapi bisa digunakan menjadi:
ShoppingCart<Book>
atau:
ShoppingCart<Cloth>
atau:
ShoppingCart<string>
atau:
ShoppingCart<number>
atau tipe lainnya.
13. Generic Class tidak hanya untuk object
Ini juga penting.
Mungkin kita berpikir Generic hanya berguna untuk object kompleks seperti:
Book
Cloth
User
Product
Padahal tidak.
Kita juga bisa menggunakan primitive type.
Misalnya:
const stringCart = new ShoppingCart<string>();
Sekarang:
T = string
Jadi:
items: string[]
dan:
addItem(item: string)
serta:
getItems(): string[]
Tambahkan string
stringCart.addItem("Hello");
stringCart.addItem("World");
Kemudian:
const values = stringCart.getItems();
values akan bertipe:
string[]
14. Bisa juga number
Misalnya:
const numberCart = new ShoppingCart<number>();
Sekarang:
T = number
Maka:
items: number[]
Kita bisa:
numberCart.addItem(10);
numberCart.addItem(20);
numberCart.addItem(30);
Tapi:
numberCart.addItem("Hello");
akan error.
Karena cart tersebut adalah:
ShoppingCart<number>
15. Gambaran besarnya
Bayangkan Generic Class seperti cetakan.
ShoppingCart<T>
│
┌─────────┼─────────┐
│ │ │
▼ ▼ ▼
Book Cloth string
│ │ │
▼ ▼ ▼
ShoppingCart ShoppingCart ShoppingCart
<Book> <Cloth> <string>
Class-nya tetap satu.
Yang berubah adalah T.
16. Kenapa tidak membuat tiga class saja?
Misalnya tanpa generic kita membuat:
class BookCart {
private items: Book[] = [];
addItem(item: Book) {
this.items.push(item);
}
getItems(): Book[] {
return this.items;
}
}
Lalu:
class ClothCart {
private items: Cloth[] = [];
addItem(item: Cloth) {
this.items.push(item);
}
getItems(): Cloth[] {
return this.items;
}
}
Dan mungkin:
class StringCart {
private items: string[] = [];
addItem(item: string) {
this.items.push(item);
}
getItems(): string[] {
return this.items;
}
}
Masalahnya adalah kode yang sama diulang-ulang.
Semua class melakukan:
items
addItem()
getItems()
Perbedaannya hanya tipe datanya.
Generic menyelesaikan masalah tersebut:
class ShoppingCart<T> {
private items: T[] = [];
addItem(item: T) {
this.items.push(item);
}
getItems(): T[] {
return this.items;
}
}
Sekarang cukup satu class.
17. Hubungan Generic Class dengan Generic Function
Sebelumnya kita belajar Generic Function:
function identity<T>(value: T): T {
return value;
}
T adalah placeholder tipe.
Contoh:
identity<string>("Hello");
berarti:
T = string
Sedangkan:
identity<number>(100);
berarti:
T = number
Generic Class konsepnya sama:
class ShoppingCart<T> {
// ...
}
Kemudian:
new ShoppingCart<Book>();
berarti:
T = Book
Jadi bedanya hanya tempat penggunaan generic-nya.
| Generic | Contoh |
|---|---|
| Function | function identity<T>() |
| Class | class ShoppingCart<T> |
| Interface | interface Box<T> |
| Type | type Response<T> = ... |
Konsep T tetap sama.
18. Generic Class vs any
Ini juga penting karena sebelumnya kita sudah membahas any.
Kita bisa saja menulis:
class ShoppingCart {
private items: any[] = [];
addItem(item: any) {
this.items.push(item);
}
getItems(): any[] {
return this.items;
}
}
Kelihatannya fleksibel.
Tapi masalahnya:
const cart = new ShoppingCart();
cart.addItem("Hello");
cart.addItem(100);
cart.addItem({
name: "John"
});
Semuanya boleh.
TypeScript tidak bisa menjaga hubungan tipe.
Dengan generic:
class ShoppingCart<T> {
private items: T[] = [];
addItem(item: T) {
this.items.push(item);
}
getItems(): T[] {
return this.items;
}
}
Kemudian:
const cart = new ShoppingCart<string>();
cart.addItem("Hello");
cart.addItem("World");
Tetapi:
cart.addItem(100);
❌ Error.
Jadi:
any= “terserah tipe apa pun.”
Generic = “tipe belum ditentukan, tetapi setelah ditentukan harus konsisten.”
19. Generic Class menjaga hubungan tipe
Ini sebenarnya salah satu manfaat terpenting Generic.
Perhatikan:
class Box<T> {
value: T;
constructor(value: T) {
this.value = value;
}
getValue(): T {
return this.value;
}
}
Kemudian:
const box = new Box<string>("Hello");
Karena:
T = string
maka:
box.value
adalah:
string
dan:
box.getValue()
juga:
string
Hubungan tersebut dipertahankan.
20. Generic Class bisa menggunakan lebih dari satu type parameter
Sama seperti Generic Function, Generic Class juga bisa memiliki beberapa generic.
Contohnya:
class Pair<T, U> {
constructor(
public first: T,
public second: U
) {}
}
Kita bisa:
const pair = new Pair<string, number>("Age", 25);
Berarti:
T = string
U = number
Sehingga:
first → string
second → number
Contoh lain:
const pair = new Pair<number, boolean>(100, true);
Berarti:
T = number
U = boolean
21. Apakah nama T wajib?
Tidak.
T hanya convention.
Kita bisa:
class ShoppingCart<Item> {
private items: Item[] = [];
addItem(item: Item) {
this.items.push(item);
}
getItems(): Item[] {
return this.items;
}
}
Ini sama dengan:
class ShoppingCart<T> {
private items: T[] = [];
addItem(item: T) {
this.items.push(item);
}
getItems(): T[] {
return this.items;
}
}
Tapi biasanya programmer menggunakan nama seperti:
T → Type
U → Type kedua
K → Key
V → Value
E → Element
Jadi kalau melihat:
class ShoppingCart<T>
langsung pahami:
“
Tadalah placeholder untuk tipe.”
22. Bagaimana TypeScript mengetahui T?
Ketika kita menulis:
const bookCart = new ShoppingCart<Book>();
kita secara eksplisit memberikan:
T = Book
Kemudian semua penggunaan T di dalam class mengikuti keputusan tersebut.
class ShoppingCart<T> {
private items: T[] = [];
addItem(item: T) {
this.items.push(item);
}
getItems(): T[] {
return this.items;
}
}
Jika:
T = Book
maka:
T[] → Book[]
item: T → item: Book
return T[] → return Book[]
Kalau:
T = Cloth
maka:
T[] → Cloth[]
item: T → item: Cloth
return T[] → return Cloth[]
23. Visualisasi substitusi T
Ini cara yang bagus untuk memahami Generic Class.
Kode:
class ShoppingCart<T> {
private items: T[] = [];
addItem(item: T) {
this.items.push(item);
}
getItems(): T[] {
return this.items;
}
}
Ketika:
ShoppingCart<Book>
anggap saja TypeScript mengganti T dengan Book:
class ShoppingCart {
private items: Book[] = [];
addItem(item: Book) {
this.items.push(item);
}
getItems(): Book[] {
return this.items;
}
}
Sedangkan:
ShoppingCart<Cloth>
secara konsep menjadi:
class ShoppingCart {
private items: Cloth[] = [];
addItem(item: Cloth) {
this.items.push(item);
}
getItems(): Cloth[] {
return this.items;
}
}
Bukan berarti TypeScript benar-benar membuat class baru seperti itu, ini hanya cara mudah untuk membayangkan bagaimana T bekerja.
24. Kapan kita sebaiknya menggunakan Generic Class?
Generic Class cocok ketika:
1. Struktur/logic class sama
Misalnya semua cart memiliki:
items
addItem()
getItems()
tetapi tipe item-nya berbeda.
2. Kita ingin reusable code
Daripada:
BookCart
ClothCart
ProductCart
UserCart
bisa dibuat:
ShoppingCart<T>
3. Kita ingin type safety
Kita ingin:
ShoppingCart<Book>
hanya menerima Book.
Dan:
ShoppingCart<Cloth>
hanya menerima Cloth.
4. Kita ingin mempertahankan hubungan tipe
Misalnya:
class Box<T> {
value: T;
getValue(): T {
return this.value;
}
}
Kalau:
Box<User>
maka getValue() juga menghasilkan User.
25. Contoh yang lebih realistis
Generic Class banyak digunakan untuk struktur data.
Misalnya kita ingin membuat Storage:
class Storage<T> {
private items: T[] = [];
add(item: T) {
this.items.push(item);
}
getAll(): T[] {
return this.items;
}
clear() {
this.items = [];
}
}
Sekarang bisa digunakan untuk User:
type User = {
id: number;
name: string;
};
const userStorage = new Storage<User>();
Kita bisa:
userStorage.add({
id: 1,
name: "John"
});
Kemudian:
const users = userStorage.getAll();
TypeScript tahu:
users: User[]
Dan class yang sama bisa digunakan untuk Product:
type Product = {
id: number;
name: string;
price: number;
};
const productStorage = new Storage<Product>();
Tidak perlu membuat:
UserStorage
ProductStorage
Cukup:
Storage<T>
26. Generic Class + Constraint
Generic Class juga bisa menggunakan constraint.
Misalnya kita ingin Repository hanya menerima object yang memiliki id.
class Repository<T extends { id: number }> {
private items: T[] = [];
add(item: T) {
this.items.push(item);
}
getById(id: number): T | undefined {
return this.items.find(item => item.id === id);
}
}
Sekarang T harus punya:
id: number
Maka ini valid:
type User = {
id: number;
name: string;
};
const userRepository = new Repository<User>();
Tetapi:
const stringRepository = new Repository<string>();
❌ Error.
Karena string tidak memiliki:
id: number
Ini menggabungkan dua konsep yang sudah kita pelajari:
Generic
+
Constraint
↓
Generic Class yang lebih aman
27. Hubungan dengan materi sebelumnya
Kalau kita susun perjalanan belajar kita:
Generic
function identity<T>(value: T): T
Kita belajar bahwa T adalah placeholder tipe.
↓
Generic Function
function swap<T>(arr: T[]): T[]
T menjaga hubungan input dan output.
↓
Generic Constraint
function expand<T extends object>(obj: T)
T boleh generic, tetapi harus memenuhi syarat.
↓
keyof
function getPropValue<T, U extends keyof T>(
obj: T,
key: U
): T[U]
U dibatasi menjadi key yang valid dari T.
↓
Generic Class
class ShoppingCart<T> {
private items: T[] = [];
}
Sekarang konsep Generic diterapkan pada class.
28. Perbedaan Generic Class dan Generic Function
Generic Function
function identity<T>(value: T): T {
return value;
}
T berlaku untuk pemanggilan function tersebut.
Misalnya:
identity<string>("Hello");
identity<number>(100);
Generic Class
class Box<T> {
constructor(public value: T) {}
}
T berlaku untuk instance/object dari class tersebut.
const stringBox = new Box<string>("Hello");
const numberBox = new Box<number>(100);
Jadi:
Generic Function
↓
T ditentukan ketika function dipanggil
Generic Class
↓
T ditentukan ketika object/instance dibuat
29. Hal paling penting dari lecture ini
Kalau harus diringkas, ada 4 hal utama.
① Class bisa memiliki generic
class ShoppingCart<T> {
}
② T adalah placeholder tipe
class ShoppingCart<T>
T belum ditentukan sampai kita menggunakan class tersebut.
③ Tipe diberikan ketika membuat instance
const bookCart = new ShoppingCart<Book>();
berarti:
T = Book
Sedangkan:
const clothCart = new ShoppingCart<Cloth>();
berarti:
T = Cloth
④ Generic membuat class reusable + type-safe
Satu class:
ShoppingCart<T>
bisa digunakan untuk:
Book
Cloth
string
number
User
Product
dan tipe lainnya
tanpa kehilangan type safety.
30. Cheat Sheet
| Syntax | Arti |
|---|---|
class Box<T> | Class memiliki generic type T |
T | Placeholder untuk tipe |
T[] | Array yang berisi tipe T |
item: T | item bertipe T |
new Box<string>() | T = string |
new Box<User>() | T = User |
T extends X | T harus memenuhi constraint X |
class Pair<T, U> | Class memiliki dua generic type |
Contoh final yang perlu kamu ingat
class ShoppingCart<T> {
private items: T[] = [];
addItem(item: T) {
this.items.push(item);
}
getItems(): T[] {
return this.items;
}
}
Kemudian:
const bookCart = new ShoppingCart<Book>();
bookCart.addItem({
name: "Harry Potter",
pages: 300,
price: 20
});
Di sini:
ShoppingCart<T>
↓
T = Book
↓
items = Book[]
↓
addItem() menerima Book
↓
getItems() mengembalikan Book[]
Sedangkan:
const clothCart = new ShoppingCart<Cloth>();
menjadi:
ShoppingCart<T>
↓
T = Cloth
↓
items = Cloth[]
↓
addItem() menerima Cloth
↓
getItems() mengembalikan Cloth[]
Dan:
const stringCart = new ShoppingCart<string>();
menjadi:
ShoppingCart<T>
↓
T = string
↓
items = string[]
↓
addItem() menerima string
↓
getItems() mengembalikan string[]
🧠 Kalimat paling gampang untuk mengingatnya:
Generic Class adalah class yang tipe datanya dibuat fleksibel menggunakan placeholder seperti
T, lalu tipe tersebut ditentukan ketika kita membuat instance dari class.
Jadi kalau kamu melihat:
class Something<T> {
...
}
langsung pikir:
“Class ini bisa bekerja dengan berbagai tipe, dan
Takan diisi tipe yang kita tentukan nanti.”
Dan bedakan dengan any:
any
↓
"terserah tipe apa"
T / Generic
↓
"tipe belum ditentukan,
tapi nanti setelah ditentukan
harus konsisten"
Itulah inti dari Generic Class.