Masukkan Password
59 Membuat Fungsi Generik
Di lecture ini kita masuk lebih dalam ke Generic Function, terutama dua hal penting:
- Cara membuat generic function dengan satu generic (
T) - Cara membuat generic function dengan banyak generic (
T,U, dst.) - Kenapa generic saja belum cukup ketika bekerja dengan object
- Sedikit pengantar tentang constraint, yang akan dibahas di lecture berikutnya.
Aku akan jelaskan dari dasar supaya alurnya benar-benar nyambung dengan materi sebelumnya.
1. Review: Apa itu Generic?
Sebelumnya kita sudah belajar:
function identity<T>(value: T): T {
return value;
}
T di sini adalah placeholder untuk tipe data.
Misalnya:
identity(10);
TypeScript akan menganggap:
T = number
Jadi secara konsep:
function identity<number>(value: number): number
Kalau:
identity("Hello");
maka:
T = string
Jadi:
identity(10)
↓
T = number
↓
return number
identity("Hello")
↓
T = string
↓
return string
Jadi generic memungkinkan kita membuat satu function yang bisa bekerja dengan banyak tipe, tetapi tetap menjaga informasi tipe datanya.
2. Generic Function dengan Array
Di lecture ini kita menggunakan contoh swap.
Tujuan function-nya adalah menukar posisi dua elemen dalam array.
Misalnya:
[1, 2, 3]
Kita ingin menukar index 0 dan 2.
Hasilnya:
[3, 2, 1]
Function-nya kira-kira:
function swap<T>(
arr: T[],
index1: number,
index2: number
): T[] {
// logic
}
Mari kita bedah satu-satu.
3. Apa Arti <T> Setelah Nama Function?
Perhatikan:
function swap<T>(...)
Bagian:
<T>
adalah generic type parameter.
Artinya:
“Function
swapini memiliki sebuah placeholder tipe data bernamaT.”
T sebenarnya bukan keyword khusus.
Kita bisa saja menulis:
function swap<Type>(...)
atau:
function swap<Item>(...)
Tetapi biasanya programmer menggunakan:
T
karena T adalah konvensi umum untuk Type.
Jadi:
function swap<T>
bisa dibayangkan seperti:
swap punya sebuah kotak kosong bernama T
T = ?
Nanti ketika function digunakan, TypeScript akan mengisi kotak tersebut.
4. T[] Artinya Apa?
Sekarang lihat:
arr: T[]
Artinya:
arradalah array yang semua elemennya bertipeT.
Misalnya kita memanggil:
swap([1, 2, 3], 0, 2);
TypeScript melihat:
[1, 2, 3]
adalah:
number[]
Maka:
T = number
Sehingga:
T[]
menjadi:
number[]
Kalau:
swap(["Hello", "Hi", "How are you"], 1, 2);
Array tersebut adalah:
string[]
Maka:
T = string
Sehingga:
T[]
menjadi:
string[]
5. Jadi Function yang Sama Bisa Dipakai untuk Banyak Tipe
Kita bisa melakukan:
const numbers = swap([1, 2, 3], 0, 2);
Di sini:
T = number
Maka:
numbers
bertipe:
number[]
Kemudian:
const names = swap(["John", "Mark", "Alice"], 0, 2);
Di sini:
T = string
Maka:
names
bertipe:
string[]
Tidak perlu membuat:
swapNumbers()
swapStrings()
swapBooleans()
swapUsers()
Cukup satu:
swap<T>()
6. Kenapa Tidak Menggunakan any?
Kita sebenarnya bisa membuat:
function swap(
arr: any[],
index1: number,
index2: number
): any[] {
// ...
}
Ini memang fleksibel.
Bisa:
swap([1, 2, 3], 0, 2);
atau:
swap(["A", "B", "C"], 0, 2);
Tetapi masalahnya adalah any.
Dengan any, TypeScript kehilangan informasi mengenai tipe data.
Misalnya:
const result = swap([1, 2, 3], 0, 2);
Dengan any[], TypeScript pada dasarnya melihat:
result: any[]
Padahal kita sebenarnya tahu bahwa hasilnya adalah:
number[]
Generic mempertahankan informasi tersebut.
7. Generic Menjaga Hubungan Input dan Output
Ini salah satu konsep paling penting dari generic.
Perhatikan:
function swap<T>(
arr: T[],
index1: number,
index2: number
): T[] {
// ...
}
Ada hubungan:
Input
T[]
↓
Output
T[]
Kalau input:
number[]
maka output:
number[]
Kalau input:
string[]
maka output:
string[]
Kalau input:
User[]
maka output:
User[]
Jadi T menjaga hubungan antara input dan output.
8. Implementasi swap
Kode lengkapnya bisa seperti ini:
function swap<T>(
arr: T[],
index1: number,
index2: number
): T[] {
if (
index1 < 0 ||
index1 >= arr.length ||
index2 < 0 ||
index2 >= arr.length
) {
throw new Error("Invalid index");
}
[arr[index1], arr[index2]] =
[arr[index2], arr[index1]];
return arr;
}
Mari kita pahami.
9. Mengecek Index
Kita punya:
index1 < 0
Artinya index tidak boleh negatif.
Misalnya:
-1
tidak valid.
Kemudian:
index1 >= arr.length
Misalnya array:
[10, 20, 30]
Panjangnya:
arr.length === 3
Index yang valid adalah:
0
1
2
Index:
3
sudah tidak valid.
Makanya kita menggunakan:
index1 >= arr.length
Hal yang sama berlaku untuk index2.
10. Kalau Index Tidak Valid
Kita melakukan:
throw new Error("Invalid index");
Artinya function akan menghentikan proses dan melempar error.
Contoh:
swap([1, 2, 3], 0, 5);
Index 5 tidak ada.
Maka:
Invalid index
11. Bagian yang Menarik: Array Destructuring
Sekarang lihat:
[arr[index1], arr[index2]] =
[arr[index2], arr[index1]];
Ini adalah destructuring assignment.
Tujuannya menukar nilai.
Misalnya:
const arr = [10, 20, 30];
Kita ingin:
index 0 = 10
index 2 = 30
Setelah ditukar:
index 0 = 30
index 2 = 10
Dengan cara biasa kita mungkin menulis:
const temp = arr[index1];
arr[index1] = arr[index2];
arr[index2] = temp;
Tetapi JavaScript menyediakan cara yang lebih singkat:
[arr[index1], arr[index2]] =
[arr[index2], arr[index1]];
12. Bagaimana Destructuring Itu Bekerja?
Misalnya:
const arr = [10, 20, 30];
Dan:
index1 = 0;
index2 = 2;
Maka:
[arr[index1], arr[index2]]
berarti:
[arr[0], arr[2]]
yaitu:
[10, 30]
Sedangkan sisi kanan:
[arr[index2], arr[index1]]
menjadi:
[arr[2], arr[0]]
yaitu:
[30, 10]
Jadi:
[10, 30] = [30, 10]
Hasil akhirnya:
[30, 20, 10]
13. Kenapa Return Type-nya T[]?
Perhatikan:
function swap<T>(
arr: T[],
index1: number,
index2: number
): T[]
Bagian:
): T[]
adalah return type.
Artinya:
Function mengembalikan array yang elemennya bertipe
T.
Jadi:
swap([1, 2, 3], 0, 2)
menghasilkan:
number[]
Sedangkan:
swap(["A", "B", "C"], 0, 2)
menghasilkan:
string[]
14. Gambaran Besar swap<T>
Bayangkan T sebagai cetakan/label tipe.
swap<T>
│
┌────────┴────────┐
│ │
input: T[] output: T[]
│ │
└───────┬─────────┘
│
tipe harus sama
Kalau dipanggil dengan:
swap([1, 2, 3], 0, 2)
maka:
T = number
number[] → swap → number[]
Kalau:
swap(["A", "B", "C"], 0, 2)
maka:
T = string
string[] → swap → string[]
15. Sekarang Masuk ke Multiple Generics
Bagian kedua lecture jauh lebih menarik.
Kita ingin membuat function untuk menggabungkan dua object.
Misalnya:
const obj1 = {
name: "John",
age: 28
};
dan:
const obj2 = {
name: "John",
gender: "male"
};
Kita ingin menggabungkannya menjadi:
{
name: "John",
age: 28,
gender: "male"
}
16. Percobaan Pertama: Menggunakan object
Kita mungkin berpikir:
function merge(obj1: object, obj2: object) {
return Object.assign(obj1, obj2);
}
Kelihatannya masuk akal.
Kita panggil:
const combined = merge(
{ name: "John", age: 28 },
{ name: "John", gender: "male" }
);
Kemudian kita ingin:
combined.name
Tetapi TypeScript bisa memberikan error.
Kenapa?
17. Masalah dengan object
Ketika kita mengatakan:
obj1: object
kita hanya memberi tahu TypeScript:
“Ini adalah sebuah object.”
Tetapi TypeScript tidak tahu property apa yang ada di dalam object tersebut.
Misalnya:
const obj: object = {
name: "John"
};
Kita sebagai manusia tahu:
obj
└── name
Tetapi TypeScript hanya tahu:
obj = object
Tidak tahu apakah ada:
name
age
gender
email
address
...
Jadi:
obj.name
tidak aman menurut TypeScript.
18. Analogi object
Bayangkan seseorang memberi kita sebuah kotak dan mengatakan:
“Ini sebuah kotak.”
Apakah kita tahu isi kotaknya?
Belum tentu.
Bisa:
📦 Kotak
├── buku
├── pensil
└── penghapus
atau:
📦 Kotak
└── laptop
atau:
📦 Kotak
└── sepatu
Begitu juga dengan:
object
TypeScript tahu:
“Ini object.”
Tetapi tidak tahu struktur detailnya.
19. Solusi: Generic
Daripada:
function merge(obj1: object, obj2: object)
kita ingin mengatakan:
“Biarkan TypeScript mengetahui tipe sebenarnya dari object pertama dan object kedua.”
Maka kita menggunakan:
function merge<T, U>(
obj1: T,
obj2: U
) {
// ...
}
Sekarang kita memiliki dua generic:
<T, U>
Kenapa dua?
Karena object pertama dan object kedua bisa memiliki tipe yang berbeda.
20. Kenapa Tidak Cukup <T>?
Misalnya kita punya:
const obj1 = {
name: "John",
age: 28
};
Tipe sederhananya:
{
name: string;
age: number;
}
Sedangkan:
const obj2 = {
name: "John",
gender: "male"
};
tipenya:
{
name: string;
gender: string;
}
Keduanya berbeda.
Maka kita bisa menggunakan:
T = tipe object pertama
U = tipe object kedua
Sehingga:
function merge<T, U>(
obj1: T,
obj2: U
)
21. Bagaimana TypeScript Mengisi T dan U?
Ketika kita melakukan:
merge(
{ name: "John", age: 28 },
{ name: "John", gender: "male" }
);
TypeScript secara konsep melakukan:
T =
{
name: string;
age: number;
}
U =
{
name: string;
gender: string;
}
Jadi:
obj1 → T
obj2 → U
22. Visualisasinya
merge<T, U>
│
├── obj1: T
│
└── obj2: U
Saat dipanggil:
merge(
{ name: "John", age: 28 },
{ name: "John", gender: "male" }
);
menjadi secara konsep:
T = { name: string; age: number }
U = { name: string; gender: string }
23. Masalah Berikutnya: Object.assign
Untuk menggabungkan object, kita menggunakan:
Object.assign(obj1, obj2);
Object.assign() mengambil property dari object kedua dan memasukkannya ke object pertama.
Contoh:
const obj1 = {
name: "John",
age: 28
};
const obj2 = {
gender: "male"
};
Object.assign(obj1, obj2);
Hasilnya:
{
name: "John",
age: 28,
gender: "male"
}
24. Tetapi Kenapa Masih Ada Error?
Kita sudah punya:
function merge<T, U>(
obj1: T,
obj2: U
) {
return Object.assign(obj1, obj2);
}
Namun TypeScript masih bisa mengeluh.
Ini membawa kita ke konsep baru:
Constraints
Masalahnya kira-kira begini.
Generic:
T
bisa berarti tipe apa saja.
Misalnya:
T = number
T = string
T = boolean
T = User
T = object
...
Jadi TypeScript tidak bisa begitu saja berasumsi:
“
Tpasti object.”
Padahal Object.assign() dalam konteks ini membutuhkan sesuatu yang bisa diperlakukan sebagai object/target.
25. Generic Tanpa Constraint
Kalau kita menulis:
function merge<T, U>(
obj1: T,
obj2: U
) {
// ...
}
kita seperti mengatakan:
T = apa saja
U = apa saja
Misalnya:
merge(10, 20);
secara generic sebenarnya bisa saja dibayangkan:
T = number
U = number
Padahal function kita sebenarnya dibuat untuk object.
Jadi kita perlu membatasi generic.
Inilah yang disebut:
Generic Constraint
26. Apa Itu Constraint?
Constraint artinya:
Memberikan batasan terhadap tipe yang boleh digunakan sebagai generic.
Tanpa constraint:
<T>
artinya kurang lebih:
T boleh tipe apa saja
Dengan constraint:
<T extends SomeType>
artinya:
T harus memenuhi aturan SomeType
Contoh sederhana:
function printLength<T extends { length: number }>(
value: T
) {
console.log(value.length);
}
Sekarang TypeScript tahu:
Tharus memiliki propertylengthyang bertipe number.
Jadi:
printLength("Hello");
boleh karena string punya length.
printLength([1, 2, 3]);
boleh karena array punya length.
Tetapi:
printLength(100);
tidak boleh karena number tidak memiliki length.
27. extends pada Generic Bukan Berarti Pewarisan Class Saja
Ini penting karena sebelumnya kita belajar:
class Dog extends Animal
Di situ extends berarti inheritance.
Tetapi pada generic:
<T extends Something>
maknanya lebih dekat ke:
“
Tharus memenuhi constraintSomething.”
Jadi:
<T extends object>
bisa dibaca:
“T harus merupakan tipe yang memenuhi batasan object.”
28. Hubungkan dengan Lecture
Lecture berhenti di sini dan mengatakan:
Kita akan membahas constraint di lecture berikutnya.
Jadi jangan merasa harus langsung menguasai bagian ini.
Yang penting sekarang pahami mengapa constraint diperlukan.
Alurnya:
Kita ingin membuat merge()
↓
obj1 dan obj2 bisa punya tipe berbeda
↓
Gunakan generic T dan U
↓
function merge<T, U>(obj1: T, obj2: U)
↓
TypeScript sekarang tahu tipe masing-masing
↓
Tapi T dan U terlalu bebas
↓
TypeScript tidak tahu bahwa T/U adalah object
↓
Perlu Generic Constraint
↓
<T extends ...>
29. Kenapa T dan U Penting untuk Object?
Mari bandingkan.
Tanpa generic
function merge(obj1: object, obj2: object) {
return Object.assign(obj1, obj2);
}
Hasilnya terlalu umum:
object
TypeScript tidak tahu property hasilnya.
Dengan generic
function merge<T, U>(obj1: T, obj2: U) {
// ...
}
Sekarang TypeScript mengetahui:
T = tipe object pertama
U = tipe object kedua
Sehingga kita bisa mempertahankan informasi tipe tersebut.
Secara konsep, hasil merge yang kita inginkan adalah gabungan:
T + U
Misalnya:
T:
{
name: string;
age: number;
}
U:
{
name: string;
gender: string;
}
Maka hasilnya secara konsep:
{
name: string;
age: number;
gender: string;
}
30. Ini Nanti Berkaitan dengan Intersection Type
Ini juga menarik karena sebelumnya kita sudah belajar Intersection Type.
Kita pernah melihat:
type A = {
name: string;
age: number;
};
type B = {
gender: string;
};
type C = A & B;
Hasilnya:
{
name: string;
age: number;
gender: string;
}
Konsep merge object secara type-level nantinya sangat berhubungan dengan ide:
T & U
Jadi pelajaran-pelajaran TypeScript ini sebenarnya saling terhubung.
31. Generic Function vs Generic Multiple Parameters
Perhatikan perbedaannya.
Satu generic
function swap<T>(arr: T[]): T[] {
// ...
}
Kita hanya membutuhkan satu placeholder karena semua elemen array memiliki tipe yang sama.
T = number
number[] → number[]
atau:
T = string
string[] → string[]
Dua generic
function merge<T, U>(
obj1: T,
obj2: U
) {
// ...
}
Kita membutuhkan dua placeholder karena object pertama dan kedua bisa berbeda.
T = tipe obj1
U = tipe obj2
32. Multiple Generic Bisa Lebih dari Dua
Tidak terbatas hanya:
<T, U>
Kita bisa memiliki:
<T, U, V>
atau:
<T, U, V, W>
Misalnya:
function triple<T, U, V>(
first: T,
second: U,
third: V
) {
return [first, second, third];
}
Pemanggilan:
triple("John", 28, true);
TypeScript bisa menginfer:
T = string
U = number
V = boolean
33. Generic Itu Seperti Template
Cara paling mudah membayangkannya:
function swap<T>(...)
seperti kita membuat cetakan:
SWAP TEMPLATE
T = tipe apa?
Ketika digunakan:
swap([1, 2, 3], 0, 2)
TypeScript mengatakan:
Oh, T = number
Kemudian:
T[] → number[]
Untuk multiple generic:
function merge<T, U>(...)
seperti:
MERGE TEMPLATE
T = tipe pertama
U = tipe kedua
Kemudian:
merge(obj1, obj2)
TypeScript mengisi:
T = tipe obj1
U = tipe obj2
34. Generic Inference
Di lecture ini sebenarnya ada proses yang disebut type inference.
Kita tidak perlu menulis:
swap<number>([1, 2, 3], 0, 2);
TypeScript bisa mengetahuinya sendiri:
swap([1, 2, 3], 0, 2);
Karena melihat:
[1, 2, 3]
adalah number[].
Begitu juga:
swap(["A", "B"], 0, 1);
TypeScript mengetahui:
T = string
Jadi generic biasanya tidak perlu kita isi secara manual.
35. Bisa Ditulis Explicit Juga
Walaupun TypeScript bisa inference, kita sebenarnya bisa menulis:
swap<number>([1, 2, 3], 0, 2);
atau:
swap<string>(["A", "B"], 0, 1);
Untuk multiple generic:
merge<
{ name: string; age: number },
{ name: string; gender: string }
>(
obj1,
obj2
);
Tetapi biasanya ini terlalu verbose.
Kita lebih sering membiarkan TypeScript melakukan inference:
merge(obj1, obj2);
36. Satu Hal Penting: Generic Tidak Mengubah Data
Misalnya:
function identity<T>(value: T): T {
return value;
}
Generic tidak mengubah:
10
menjadi:
"10"
atau:
number
menjadi:
string
Generic hanya membantu TypeScript memahami hubungan tipe.
Jadi generic adalah fitur TypeScript untuk type system, bukan mekanisme konversi data runtime.
37. Hubungan any, object, dan Generic
Ini penting untuk membedakan ketiganya.
any
function test(value: any) {}
Artinya:
“Saya tidak peduli tipenya.”
Type safety melemah.
object
function test(value: object) {}
Artinya:
“Saya tahu ini object.”
Tetapi belum tentu tahu property apa yang dimiliki.
Generic
function test<T>(value: T): T {}
Artinya:
“Saya belum tahu tipenya, tetapi saya ingin mempertahankan informasi tipe tersebut.”
Generic + Constraint
function test<T extends object>(value: T): T {}
Artinya:
“Saya belum tahu tipe detailnya, tetapi saya tahu T harus memenuhi batasan object.”
Ini jauh lebih powerful.
38. Perbandingan Besar
| Konsep | Makna sederhana |
|---|---|
any | “Terserah, saya tidak peduli tipenya” |
object | “Ini object, tapi detail property belum diketahui” |
T | “Tipenya belum ditentukan, nanti kita cari tahu” |
T extends X | “Tipenya belum ditentukan, tapi harus memenuhi aturan X” |
T, U | “Ada dua tipe yang bisa berbeda” |
39. Kesalahan yang Sering Terjadi
Kesalahan 1 — Mengira T adalah tipe khusus
function test<T>(value: T) {}
T bukan tipe seperti string atau number.
T adalah placeholder.
Kesalahan 2 — Mengira generic selalu harus bernama T
Tidak.
Ini valid:
function test<Type>(value: Type): Type {
return value;
}
Ini juga:
function test<Item>(value: Item): Item {
return value;
}
Tetapi konvensinya memang sering:
T
U
V
Kesalahan 3 — Menggunakan satu generic untuk tipe yang seharusnya berbeda
Misalnya:
function merge<T>(obj1: T, obj2: T) {}
Ini berarti:
obj1danobj2harus memiliki tipe yang sama.
Padahal kita ingin:
obj1 = tipe A
obj2 = tipe B
Maka lebih cocok:
function merge<T, U>(obj1: T, obj2: U) {}
40. Ringkasan Lecture dalam Satu Alur
Lecture ini sebenarnya sedang mengajarkan evolusi berikut:
Tahap 1 — Function biasa
function swap(arr: any[]) {}
Fleksibel, tetapi kehilangan type safety.
Tahap 2 — Generic
function swap<T>(arr: T[]): T[] {}
Sekarang tipe array dipertahankan.
number[] → number[]
string[] → string[]
Tahap 3 — Multiple Generic
function merge<T, U>(obj1: T, obj2: U) {}
Sekarang kita bisa menangani dua input dengan tipe berbeda.
obj1 → T
obj2 → U
Tahap 4 — Constraint
Masalah:
T dan U terlalu bebas
Kita ingin:
T harus object
U harus object
Maka kita membutuhkan:
T extends ...
U extends ...
Dan inilah materi lecture berikutnya.
41. Cheat Sheet
Generic function
function identity<T>(value: T): T {
return value;
}
Generic array
function swap<T>(arr: T[]): T[] {
return arr;
}
Multiple generics
function pair<T, U>(
first: T,
second: U
) {
return [first, second];
}
Generic interface
interface Box<T> {
value: T;
}
Generic constraint
function printLength<T extends { length: number }>(
value: T
) {
console.log(value.length);
}
42. Kalimat yang Paling Penting untuk Diingat
Kalau harus mengingat inti lecture ini, ingat empat kalimat berikut:
<T>adalah placeholder untuk tipe data.
Generic memungkinkan satu function bekerja dengan banyak tipe tanpa kehilangan type safety.
<T, U>digunakan ketika kita mempunyai beberapa tipe yang bisa berbeda.
extendspada generic digunakan untuk memberikan constraint/batasan terhadap tipe yang boleh digunakan.
Dan hubungan dengan lecture sebelumnya:
Generic
↓
T = placeholder tipe
↓
Generic Function
↓
function swap<T>(...)
↓
Multiple Generic
↓
function merge<T, U>(...)
↓
Masalah: T/U terlalu bebas
↓
Generic Constraint
↓
<T extends ...>
Jadi sebenarnya lecture ini adalah jembatan dari Generic → Generic Constraint. Bagian swap<T> relatif sederhana; bagian merge<T, U> mulai menunjukkan alasan kenapa kita membutuhkan constraint, supaya TypeScript tidak hanya tahu “ini tipe apa”, tetapi juga tahu aturan apa yang wajib dimiliki tipe tersebut.