Fungsi #
Fungsi adalah unit dasar dari kode yang dapat digunakan kembali — dan di TypeScript, fungsi mendapat perlakuan istimewa dari sistem tipe. Setiap aspek fungsi bisa — dan sebaiknya — dianotasi: parameter, nilai kembalian, bahkan fungsi yang diterima sebagai argumen. TypeScript juga memperluas kapabilitas fungsi JavaScript dengan overloading yang nyata, generic yang fleksibel, dan integrasi async-await yang type-safe. Memahami cara menulis fungsi yang benar di TypeScript bukan hanya tentang menambahkan anotasi tipe — tapi tentang merancang kontrak yang jelas antara pemanggil dan implementasi, sehingga compiler bisa membantu mendeteksi ketidaksesuaian sebelum kode dijalankan.
Anatomi Fungsi TypeScript #
Sebelum masuk ke variasi, penting memahami semua bagian yang bisa dianotasi dalam sebuah fungsi:
// ┌── nama fungsi
// │ ┌── parameter dengan tipe
// │ │ ┌── return type
// ▼ ▼ ▼
function hitung(a: number, b: number): number {
return a + b; // Nilai kembalian harus sesuai return type
}
// TypeScript menyimpulkan return type jika tidak ditulis eksplisit
// — tapi untuk fungsi publik, selalu tulis eksplisit agar API jelas
Mengapa return type eksplisit penting untuk fungsi publik?
// ANTI-PATTERN: Tanpa return type eksplisit — bug tersembunyi
function cariPengguna(id: number) {
if (id > 0) {
return { nama: "Budi", email: "[email protected]" };
}
// Implisit return undefined — TypeScript menyimpulkan return type sebagai
// { nama: string; email: string } | undefined
// Tapi pengguna fungsi ini tidak tahu ada kemungkinan undefined!
}
// BENAR: Return type eksplisit — kontrak fungsi jelas
function cariPengguna2(id: number): { nama: string; email: string } | null {
if (id > 0) {
return { nama: "Budi", email: "[email protected]" };
}
return null; // ✓ Kompiler memastikan semua jalur return mengembalikan tipe yang benar
}
Deklarasi Fungsi vs Ekspresi Fungsi vs Arrow Function #
Ada tiga cara mendefinisikan fungsi di TypeScript, masing-masing dengan karakteristik yang berbeda:
// 1. Deklarasi fungsi — di-hoist ke atas scope
function tambah(a: number, b: number): number {
return a + b;
}
// 2. Ekspresi fungsi — tidak di-hoist, disimpan dalam variabel
const kurang = function (a: number, b: number): number {
return a - b;
};
// 3. Arrow function — sintaks ringkas, this lexical (tidak punya this sendiri)
const kali = (a: number, b: number): number => a * b;
// Arrow function multi-baris
const bagi = (a: number, b: number): number => {
if (b === 0) throw new Error("Tidak bisa dibagi nol");
return a / b;
};
Perbedaan this — Kritis untuk Method Class
#
Perbedaan terpenting antara arrow function dan function biasa adalah cara mereka menangani this. Arrow function mewarisi this dari scope di mana ia didefinisikan (lexical this), sementara function biasa punya this sendiri yang bergantung pada cara pemanggilan:
class Timer {
private hitungan = 0;
// ANTI-PATTERN: Menggunakan function biasa sebagai callback
// 'this' di dalam callback tidak merujuk ke instance Timer
mulaiSalah(): void {
setInterval(function () {
this.hitungan++; // ✗ 'this' adalah undefined di strict mode
console.log(this.hitungan);
}, 1000);
}
// BENAR: Arrow function mewarisi 'this' dari Timer
mulaiBenar(): void {
setInterval(() => {
this.hitungan++; // ✓ 'this' adalah instance Timer
console.log(this.hitungan);
}, 1000);
}
}
Tipe Fungsi sebagai Variabel #
Kamu bisa mendefinisikan tipe sebuah fungsi secara eksplisit menggunakan sintaks (params) => ReturnType:
// Mendefinisikan tipe fungsi
type OperasiMatematika = (a: number, b: number) => number;
type Validator<T> = (nilai: T) => boolean;
type Transformer<TInput, TOutput> = (input: TInput) => TOutput;
// Variabel dengan tipe fungsi
const operasi: OperasiMatematika = (a, b) => a + b;
// TypeScript tahu a dan b adalah number — tidak perlu anotasi ulang
const validasiEmail: Validator<string> = (email) =>
/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email);
const keString: Transformer<number, string> = (n) => n.toString();
Parameter: Opsional, Default, dan Rest #
Parameter Opsional dengan ?
#
Parameter opsional ditandai dengan ? dan selalu bertipe T | undefined di dalam fungsi:
function buatProfil(
nama: string,
usia?: number, // Tipe di dalam fungsi: number | undefined
kota?: string // Tipe di dalam fungsi: string | undefined
): string {
const bagianUsia = usia !== undefined ? `, ${usia} tahun` : "";
const bagianKota = kota ? ` dari ${kota}` : "";
return `${nama}${bagianUsia}${bagianKota}`;
}
console.log(buatProfil("Budi")); // "Budi"
console.log(buatProfil("Budi", 25)); // "Budi, 25 tahun"
console.log(buatProfil("Budi", 25, "Jakarta")); // "Budi, 25 tahun dari Jakarta"
// ATURAN: Parameter opsional harus selalu di akhir daftar parameter
// function salah(nama?: string, usia: number): string {} // ✗ Error
Parameter Default #
Parameter dengan nilai default memberikan nilai fallback ketika argumen tidak diberikan atau diberikan undefined:
function kirimEmail(
tujuan: string,
subjek: string,
prioritas: "rendah" | "normal" | "tinggi" = "normal",
maxPercobaan: number = 3
): void {
console.log(`Kirim ke ${tujuan}: "${subjek}" [${prioritas}] (max ${maxPercobaan}x)`);
}
kirimEmail("[email protected]", "Selamat Datang");
// "Kirim ke [email protected]: "Selamat Datang" [normal] (max 3x)"
kirimEmail("[email protected]", "Peringatan Kritis", "tinggi");
// "Kirim ke [email protected]: "Peringatan Kritis" [tinggi] (max 3x)"
// Mengirim undefined secara eksplisit akan menggunakan nilai default
kirimEmail("[email protected]", "Info", undefined, 5);
// "Kirim ke [email protected]: "Info" [normal] (max 5x)"
Rest Parameter #
Rest parameter mengumpulkan semua argumen yang tersisa ke dalam array. Harus selalu menjadi parameter terakhir:
// Rest parameter dasar
function jumlahkan(...angka: number[]): number {
return angka.reduce((total, n) => total + n, 0);
}
console.log(jumlahkan(1, 2, 3)); // 6
console.log(jumlahkan(10, 20, 30, 40)); // 100
// Kombinasi parameter biasa dan rest
function log(level: "info" | "warn" | "error", ...pesan: string[]): void {
const timestamp = new Date().toISOString();
const teksLengkap = pesan.join(" ");
console.log(`[${timestamp}] [${level.toUpperCase()}] ${teksLengkap}`);
}
log("info", "Server", "dimulai", "di port 3000");
log("error", "Koneksi database gagal:", "timeout setelah 30 detik");
Overloading — Satu Fungsi, Banyak Signature #
Overloading di TypeScript memungkinkan satu fungsi memiliki beberapa signature tipe yang berbeda. Ini berbeda dari bahasa seperti Java — di TypeScript, overloading hanya ada di level tipe, bukan di level implementasi:
// Signature-signature overload (tidak memiliki implementasi)
function format(nilai: number): string;
function format(nilai: string): string;
function format(nilai: Date): string;
// Implementasi tunggal yang menangani semua signature
function format(nilai: number | string | Date): string {
if (typeof nilai === "number") {
return nilai.toLocaleString("id-ID");
}
if (typeof nilai === "string") {
return nilai.trim();
}
// Sisa: nilai adalah Date
return nilai.toLocaleDateString("id-ID", {
weekday: "long",
year: "numeric",
month: "long",
day: "numeric",
});
}
// TypeScript menampilkan signature yang relevan berdasarkan argumen
console.log(format(1_500_000)); // "1.500.000"
console.log(format(" Halo Dunia ")); // "Halo Dunia"
console.log(format(new Date())); // "Kamis, 7 Mei 2026"
Overloading dengan Jumlah Parameter Berbeda #
// Overload untuk membuat rentang angka
function rentang(selesai: number): number[];
function rentang(mulai: number, selesai: number): number[];
function rentang(mulai: number, selesai: number, langkah: number): number[];
function rentang(mulaiAtauSelesai: number, selesai?: number, langkah = 1): number[] {
const [mulai, akhir] =
selesai === undefined ? [0, mulaiAtauSelesai] : [mulaiAtauSelesai, selesai];
const hasil: number[] = [];
for (let i = mulai; i < akhir; i += langkah) {
hasil.push(i);
}
return hasil;
}
console.log(rentang(5)); // [0, 1, 2, 3, 4]
console.log(rentang(2, 7)); // [2, 3, 4, 5, 6]
console.log(rentang(0, 10, 2)); // [0, 2, 4, 6, 8]
Higher-Order Function dan Closure #
Higher-order function (HOF) adalah fungsi yang menerima fungsi lain sebagai argumen atau mengembalikan fungsi. Ini adalah pola fundamental dalam pemrograman fungsional dan sangat umum di TypeScript:
// Fungsi yang menerima fungsi sebagai parameter
function transformArray<T, U>(
arr: T[],
transformFn: (item: T, indeks: number) => U
): U[] {
return arr.map(transformFn);
}
const angka = [1, 2, 3, 4, 5];
const dikuadratkan = transformArray(angka, (n) => n ** 2);
// Tipe: number[] → [1, 4, 9, 16, 25]
const daftarNama = ["budi", "siti", "ahmad"];
const dikapitalisasi = transformArray(daftarNama, (nama, i) =>
`${i + 1}. ${nama.charAt(0).toUpperCase()}${nama.slice(1)}`
);
// Tipe: string[] → ["1. Budi", "2. Siti", "3. Ahmad"]
// Fungsi yang mengembalikan fungsi (closure)
function buatPenghitung(mulaiDari: number = 0) {
let hitungan = mulaiDari;
return {
tambah: () => ++hitungan,
kurang: () => --hitungan,
reset: () => { hitungan = mulaiDari; },
nilai: () => hitungan,
};
}
const counter = buatPenghitung(10);
counter.tambah(); // 11
counter.tambah(); // 12
counter.kurang(); // 11
console.log(counter.nilai()); // 11
Currying — Fungsi yang Mengembalikan Fungsi #
Currying mengubah fungsi dengan banyak parameter menjadi rangkaian fungsi satu parameter:
// Fungsi biasa
function tambah(a: number, b: number): number {
return a + b;
}
// Versi curried — bisa diaplikasikan sebagian
function tambahCurried(a: number) {
return (b: number): number => a + b;
}
const tambah5 = tambahCurried(5); // Fungsi yang selalu menambahkan 5
console.log(tambah5(3)); // 8
console.log(tambah5(10)); // 15
// Pola currying sangat berguna untuk konfigurasi awal
function buatValidator(panjangMin: number, panjangMaks: number) {
return (teks: string): boolean =>
teks.length >= panjangMin && teks.length <= panjangMaks;
}
const validasiNama = buatValidator(2, 50);
const validasiPassword = buatValidator(8, 128);
const validasiBio = buatValidator(0, 160);
console.log(validasiNama("Budi")); // true
console.log(validasiPassword("abc")); // false — terlalu pendek
Generic Function — Fungsi untuk Semua Tipe #
Generic memungkinkan fungsi bekerja dengan berbagai tipe sambil tetap mempertahankan type safety. TypeScript menyimpulkan tipe generic dari argumen yang diberikan:
// Generic dasar — T adalah "placeholder" tipe
function identitas<T>(nilai: T): T {
return nilai;
}
const angka = identitas(42); // T disimpulkan: number
const teks = identitas("halo"); // T disimpulkan: string
const obj = identitas({ a: 1 }); // T disimpulkan: { a: number }
// Generic dengan constraint — T harus memiliki properti tertentu
function ambilPertama<T extends { panjang: number }>(koleksi: T): T {
if (koleksi.panjang === 0) throw new Error("Koleksi kosong");
return koleksi;
}
// Generic dengan multiple type parameter
function pasangkan<TKunci, TNilai>(kunci: TKunci, nilai: TNilai): [TKunci, TNilai] {
return [kunci, nilai];
}
const pasangan = pasangkan("nama", "Budi"); // Tipe: [string, string]
const campuran = pasangkan(1, true); // Tipe: [number, boolean]
// Generic dengan kondisi (conditional type) — lanjutan
function pertama<T>(arr: T[]): T | undefined {
return arr[0];
}
function pertamaPasti<T>(arr: [T, ...T[]]): T {
// Parameter bertipe non-empty tuple — menjamin array tidak kosong
return arr[0];
}
const nilaiAman = pertama([1, 2, 3]); // Tipe: number | undefined
const nilaiPasti = pertamaPasti([1, 2, 3]); // Tipe: number (tidak ada undefined)
// pertamaPasti([]); // ✗ Error kompilasi — array harus ada elemen
Fungsi Async-Await dan Penanganan Error #
Fungsi async mengembalikan Promise<T> — TypeScript memastikan tipe yang di-await sesuai dengan tipe yang dideklarasikan:
interface DataPengguna {
id: number;
nama: string;
email: string;
}
// Fungsi async dasar — return type Promise<DataPengguna>
async function ambilPengguna(id: number): Promise<DataPengguna> {
const response = await fetch(`https://api.example.com/pengguna/${id}`);
if (!response.ok) {
throw new Error(`Gagal mengambil data: HTTP ${response.status}`);
}
const data = await response.json();
return data as DataPengguna; // Type assertion setelah validasi response.ok
}
// Pemanggilan dengan try-catch
async function tampilkanPengguna(id: number): Promise<void> {
try {
const pengguna = await ambilPengguna(id);
console.log(`Nama: ${pengguna.nama}`);
console.log(`Email: ${pengguna.email}`);
} catch (error) {
// Di catch block, error bertipe 'unknown' — harus di-narrow dulu
if (error instanceof Error) {
console.error(`Error: ${error.message}`);
} else {
console.error("Terjadi error yang tidak diketahui");
}
}
}
Pola Result untuk Error Handling yang Lebih Aman #
try-catch memaksa penanganan error terpisah dari alur normal. Pola Result mengembalikan sukses atau error sebagai nilai biasa:
type Result<T, E = Error> =
| { sukses: true; data: T }
| { sukses: false; error: E };
async function ambilPenggunaSafe(id: number): Promise<Result<DataPengguna>> {
try {
const response = await fetch(`https://api.example.com/pengguna/${id}`);
if (!response.ok) {
return {
sukses: false,
error: new Error(`HTTP ${response.status}: ${response.statusText}`),
};
}
const data = await response.json();
return { sukses: true, data: data as DataPengguna };
} catch (error) {
return {
sukses: false,
error: error instanceof Error ? error : new Error(String(error)),
};
}
}
// Penggunaan — tanpa try-catch, error ditangani inline
async function prosesPengguna(id: number): Promise<void> {
const hasil = await ambilPenggunaSafe(id);
if (!hasil.sukses) {
console.error(`Gagal: ${hasil.error.message}`);
return;
}
// TypeScript tahu hasil.data bertipe DataPengguna di sini
console.log(`Selamat datang, ${hasil.data.nama}!`);
}
Menjalankan Promise Secara Paralel #
async function ambilSemuaData(): Promise<void> {
// ANTI-PATTERN: Await serial — lambat karena menunggu satu per satu
const pengguna1 = await ambilPengguna(1); // Tunggu...
const pengguna2 = await ambilPengguna(2); // Baru mulai setelah 1 selesai
const pengguna3 = await ambilPengguna(3); // Baru mulai setelah 2 selesai
// BENAR: Promise.all — paralel, lebih cepat
const [p1, p2, p3] = await Promise.all([
ambilPengguna(1), // Semua dimulai bersamaan
ambilPengguna(2),
ambilPengguna(3),
]);
// Tipe: [DataPengguna, DataPengguna, DataPengguna]
// Promise.allSettled — lanjut meski ada yang gagal
const hasil = await Promise.allSettled([
ambilPengguna(1),
ambilPengguna(999), // ID tidak valid — akan rejected
ambilPengguna(3),
]);
for (const r of hasil) {
if (r.status === "fulfilled") {
console.log(`Berhasil: ${r.value.nama}`);
} else {
console.error(`Gagal: ${r.reason}`);
}
}
}
Peta Konsep: Jenis-Jenis Fungsi TypeScript #
flowchart TD
A[Fungsi TypeScript] --> B[Berdasarkan Definisi]
A --> C[Berdasarkan Parameter]
A --> D[Berdasarkan Pola]
B --> B1[Deklarasi function]
B --> B2[Ekspresi function]
B --> B3[Arrow function]
C --> C1[Parameter wajib]
C --> C2[Parameter opsional ?]
C --> C3[Default parameter]
C --> C4[Rest parameter ...]
C --> C5[Destructured parameter]
D --> D1[Higher-order function]
D --> D2[Generic function T]
D --> D3[Overloaded function]
D --> D4[Async function]
D --> D5[Generator function*]
D --> D6[Curried function]
B3 -- Lexical this --> E[Cocok untuk callback\ndan method arrow]
B1 -- Hoisted --> F[Bisa dipanggil\nsebelum deklarasi]
D4 -- Returns --> G[Promise di T ]
D5 -- Yields --> H[Generator di T ]
Ringkasan #
- Selalu tulis return type eksplisit untuk fungsi publik — ini mendefinisikan kontrak yang jelas dan memaksa semua jalur return mengembalikan tipe yang benar; biarkan TypeScript menyimpulkan return type hanya untuk fungsi private atau arrow function pendek.
- Arrow function untuk callback — arrow function mewarisi
thisdari scope luar (lexical this), membuatnya aman digunakan sebagai callback dalam method class; function biasa punyathissendiri yang bisa mengejutkan.- Parameter opsional
?vs default=— parameter opsional harus di-narrow (!== undefined) sebelum digunakan; parameter default lebih praktis karena langsung punya nilai fallback tanpa pengecekan manual.- Rest parameter untuk argumen variadic — kumpulkan argumen tak terbatas dengan
...nama: T[]; selalu jadikan parameter terakhir karena tidak boleh ada parameter lain sesudahnya.- Overloading untuk API yang ekspresif — tulis beberapa signature overload tanpa implementasi, lalu satu implementasi yang menangani semua signature; ini memberi IntelliSense yang akurat kepada pengguna fungsi.
- Generic
<T>untuk fleksibilitas tanpa kehilangan tipe — gunakan type parameter saat implementasi sama untuk berbagai tipe; tambahkanextendssebagai constraint jika fungsi membutuhkan properti atau metode tertentu.- Pola Result lebih aman dari try-catch untuk alur bisnis — kembalikan
{ sukses: true; data: T } | { sukses: false; error: E }agar caller dipaksa menangani error tanpa bisa melupakannya.Promise.alluntuk operasi paralel — jangan menunggu promise satu per satu jika tidak ada dependensi antar operasi;Promise.alljauh lebih efisien, danPromise.allSettleduntuk kasus di mana kegagalan parsial masih bisa diproses.- Error di
catchblock bertipeunknown— denganstrict: true, TypeScript tidak mengizinkan akses langsung padaerrordi catch; selalu narrow denganinstanceof Errorsebelum mengakseserror.message.