I/O #

Operasi I/O (Input/Output) adalah tentang bagaimana program berinteraksi dengan dunia luar — membaca dan menulis file, berkomunikasi lewat jaringan, menerima input dari pengguna. Di TypeScript, I/O hampir selalu bersifat asinkron karena sifat single-threaded JavaScript: operasi I/O yang memblokir akan membekukan seluruh event loop dan membuat aplikasi tidak responsif. Node.js menggunakan libuv untuk mendelegasikan operasi I/O ke sistem operasi secara non-blocking, sedangkan browser menggunakan Web APIs. TypeScript menambahkan lapisan type safety di atas semua ini — tipe kembalian yang akurat, penanganan error yang eksplisit, dan generics untuk response data. Memahami I/O di TypeScript berarti memahami kapan async benar-benar diperlukan, bagaimana menangani error I/O dengan benar, dan bagaimana streaming bekerja untuk data yang terlalu besar untuk dimuat sekaligus ke memori.

Async vs Sync I/O — Pilihan yang Menentukan #

Node.js menyediakan dua versi untuk hampir setiap operasi I/O: versi async (non-blocking) dan sync (blocking). Keduanya punya tempat yang tepat:

import * as fs from "fs";
import { readFile, writeFile } from "fs/promises";

// SINKRON — Memblokir event loop hingga selesai
// Kapan tepat: Script CLI satu-off, inisialisasi sebelum server dimulai
try {
  const isi = fs.readFileSync("konfigurasi.json", "utf-8");
  const konfig = JSON.parse(isi);
  console.log("Konfigurasi dimuat:", konfig);
} catch (err) {
  console.error("Gagal memuat konfigurasi:", err);
  process.exit(1);
}

// ASINKRON — Tidak memblokir event loop
// Kapan tepat: Hampir semua kasus dalam server/aplikasi produksi
async function muatKonfigurasi(): Promise<Record<string, unknown>> {
  try {
    const isi = await readFile("konfigurasi.json", "utf-8");
    return JSON.parse(isi) as Record<string, unknown>;
  } catch (err) {
    if ((err as NodeJS.ErrnoException).code === "ENOENT") {
      console.warn("File konfigurasi tidak ditemukan, menggunakan default");
      return {};
    }
    throw err;
  }
}
Jangan pernah gunakan operasi sync I/O di dalam server yang sedang melayani requestreadFileSync, writeFileSync, execSync semuanya memblokir event loop dan membuat server tidak bisa melayani request lain selama operasi berlangsung. Gunakan varian async atau streaming.

Operasi File dengan fs/promises #

fs/promises adalah modul yang mengekspos semua fungsi fs sebagai Promise — jauh lebih bersih daripada callback-based API lama:

Membaca File #

import { readFile, stat, access, constants } from "fs/promises";
import path from "path";

// Baca file teks
async function bacaFileTeks(filePath: string): Promise<string> {
  try {
    return await readFile(filePath, "utf-8");
  } catch (err) {
    const error = err as NodeJS.ErrnoException;
    switch (error.code) {
      case "ENOENT":
        throw new Error(`File tidak ditemukan: ${filePath}`);
      case "EACCES":
        throw new Error(`Tidak ada izin membaca: ${filePath}`);
      default:
        throw new Error(`Gagal membaca file: ${error.message}`);
    }
  }
}

// Baca file JSON dengan type safety
async function bacaJSON<T>(filePath: string): Promise<T> {
  const isi = await readFile(filePath, "utf-8");
  try {
    return JSON.parse(isi) as T;
  } catch {
    throw new SyntaxError(`File ${filePath} bukan JSON yang valid`);
  }
}

// Baca file biner (gambar, PDF, dll)
async function bacaFileBiner(filePath: string): Promise<Buffer> {
  return readFile(filePath); // Tanpa encoding = Buffer
}

// Cek apakah file ada sebelum membaca
async function fileAda(filePath: string): Promise<boolean> {
  try {
    await access(filePath, constants.F_OK);
    return true;
  } catch {
    return false;
  }
}

// Ambil informasi file
async function infoFile(filePath: string) {
  const info = await stat(filePath);
  return {
    ukuran: info.size,
    dibuatPada: info.birthtime,
    dimodifikasiPada: info.mtime,
    adalahFile: info.isFile(),
    adalahDirektori: info.isDirectory(),
  };
}
import { writeFile, appendFile, mkdir, copyFile } from "fs/promises";

// Tulis file (buat baru atau timpa yang ada)
async function tulisFile(filePath: string, isi: string): Promise<void> {
  // Pastikan direktori ada sebelum menulis
  await mkdir(path.dirname(filePath), { recursive: true });
  await writeFile(filePath, isi, "utf-8");
}

// Tulis objek sebagai JSON yang terformat
async function tulisJSON<T>(filePath: string, data: T): Promise<void> {
  const json = JSON.stringify(data, null, 2); // 2 spasi indentasi
  await tulisFile(filePath, json);
}

// Tambahkan ke akhir file (untuk logging, csv, dll)
async function tambahKeFile(filePath: string, baris: string): Promise<void> {
  await appendFile(filePath, baris + "\n", "utf-8");
}

// Tulis dengan backup — ganti nama file lama, tulis yang baru
async function tulisDenganBackup(
  filePath: string,
  isi: string
): Promise<void> {
  const backupPath = `${filePath}.bak`;

  if (await fileAda(filePath)) {
    await copyFile(filePath, backupPath);
  }

  await writeFile(filePath, isi, "utf-8");
}

Operasi Direktori #

import { readdir, rm, rename } from "fs/promises";

// Daftar semua file dalam direktori dengan filter
async function daftarFile(
  dirPath: string,
  ekstensi?: string
): Promise<string[]> {
  const entri = await readdir(dirPath, { withFileTypes: true });

  return entri
    .filter((e) => e.isFile())
    .map((e) => e.name)
    .filter((nama) => !ekstensi || nama.endsWith(ekstensi));
}

// Daftar rekursif semua file
async function daftarFilesRekursif(dirPath: string): Promise<string[]> {
  const entri = await readdir(dirPath, { withFileTypes: true });
  const files: string[] = [];

  for (const entri_ of entri) {
    const fullPath = path.join(dirPath, entri_.name);
    if (entri_.isDirectory()) {
      const subFiles = await daftarFilesRekursif(fullPath);
      files.push(...subFiles);
    } else {
      files.push(fullPath);
    }
  }

  return files;
}

// Hapus file atau direktori
async function hapus(targetPath: string): Promise<void> {
  await rm(targetPath, { recursive: true, force: true });
}

// Rename / pindahkan file
async function pindahkanFile(dari: string, ke: string): Promise<void> {
  await mkdir(path.dirname(ke), { recursive: true });
  await rename(dari, ke);
}

Streaming — Untuk File Besar #

Membaca seluruh file ke memori dengan readFile tidak scalable untuk file besar. Streaming memproses data sepotong demi sepotong:

import { createReadStream, createWriteStream } from "fs";
import { pipeline } from "stream/promises";
import { Transform } from "stream";

// Stream baca file baris per baris — efisien untuk file log besar
import * as readline from "readline";

async function prosesFileBesar(filePath: string): Promise<void> {
  const fileStream = createReadStream(filePath, { encoding: "utf-8" });
  const rl = readline.createInterface({
    input: fileStream,
    crlfDelay: Infinity, // Tangani line ending Windows (\r\n)
  });

  let nomorBaris = 0;
  for await (const baris of rl) {
    nomorBaris++;
    // Proses setiap baris tanpa memuat seluruh file ke memori
    if (baris.includes("ERROR")) {
      console.log(`Baris ${nomorBaris}: ${baris}`);
    }
  }

  console.log(`Selesai memproses ${nomorBaris} baris`);
}

// Pipeline stream: baca → transformasi → tulis
async function transformasiFile(
  inputPath: string,
  outputPath: string
): Promise<void> {
  const readStream = createReadStream(inputPath, { encoding: "utf-8" });
  const writeStream = createWriteStream(outputPath, { encoding: "utf-8" });

  // Transform stream: ubah setiap chunk
  const uppercase = new Transform({
    transform(chunk: Buffer, _encoding, callback) {
      callback(null, chunk.toString().toUpperCase());
    },
  });

  // pipeline menangani error dan cleanup secara otomatis
  await pipeline(readStream, uppercase, writeStream);
  console.log("Transformasi selesai");
}

Path Handling yang Aman #

Menggabungkan path secara manual dengan string concatenation adalah sumber bug — terutama di Windows yang menggunakan \ sebagai separator:

import path from "path";

// ANTI-PATTERN: Concatenation manual — tidak cross-platform
const pathSalah = __dirname + "/data/" + namaFile; // Bisa salah di Windows

// BENAR: Gunakan path.join atau path.resolve
const pathBenar = path.join(__dirname, "data", namaFile);

// path.resolve — menyelesaikan ke path absolut
const pathAbsolut = path.resolve("./data", namaFile);

// Utilitas path yang berguna
console.log(path.basename("/home/user/file.txt"));        // "file.txt"
console.log(path.basename("/home/user/file.txt", ".txt")); // "file" — tanpa ekstensi
console.log(path.dirname("/home/user/file.txt"));          // "/home/user"
console.log(path.extname("/home/user/file.txt"));          // ".txt"
console.log(path.parse("/home/user/file.txt"));
// { root: '/', dir: '/home/user', base: 'file.txt', ext: '.txt', name: 'file' }

// Path joining yang aman
const baseDir = "/data/proyek";
const userInput = "../../etc/passwd"; // Path traversal attack attempt!

// ANTI-PATTERN: Langsung join path dari user input
const pathBerbahaya = path.join(baseDir, userInput);
// Bisa keluar dari direktori yang diizinkan!

// BENAR: Validasi path tidak keluar dari direktori yang diizinkan
function pathAman(baseDir: string, userPath: string): string {
  const resolved = path.resolve(baseDir, userPath);
  if (!resolved.startsWith(path.resolve(baseDir))) {
    throw new Error(`Path traversal terdeteksi: ${userPath}`);
  }
  return resolved;
}

Input Terminal dengan readline #

import * as readline from "readline";
import { promisify } from "util";

// Interface readline standar
const rl = readline.createInterface({
  input: process.stdin,
  output: process.stdout,
});

// Promisify rl.question untuk async/await
function tanya(pertanyaan: string): Promise<string> {
  return new Promise((resolve) => {
    rl.question(pertanyaan, (jawaban) => {
      resolve(jawaban);
    });
  });
}

// Contoh: dialog interaktif multi-langkah
async function dialogPendaftaran(): Promise<void> {
  try {
    const nama = await tanya("Nama lengkap: ");
    const email = await tanya("Email: ");
    const konfirmasi = await tanya(`Daftar sebagai ${nama} <${email}>? (y/n): `);

    if (konfirmasi.toLowerCase() === "y") {
      console.log(`\nTerima kasih telah mendaftar, ${nama}!`);
      // Proses pendaftaran...
    } else {
      console.log("\nPendaftaran dibatalkan.");
    }
  } finally {
    rl.close(); // Selalu tutup interface di finally
  }
}

dialogPendaftaran().catch(console.error);

Fetch API — HTTP I/O yang Type-Safe #

// Tipe untuk response API
interface ResponsePaginasi<T> {
  data: T[];
  total: number;
  halaman: number;
  perHalaman: number;
}

interface Pengguna {
  id: number;
  nama: string;
  email: string;
}

// Fetch wrapper yang type-safe dengan error handling yang benar
async function ambilAPI<T>(
  url: string,
  opsi?: RequestInit
): Promise<T> {
  const response = await fetch(url, {
    headers: { "Content-Type": "application/json" },
    ...opsi,
  });

  if (!response.ok) {
    const pesanError = await response.text().catch(() => "Tidak ada pesan error");
    throw new Error(
      `HTTP ${response.status} ${response.statusText}: ${pesanError}`
    );
  }

  return response.json() as Promise<T>;
}

// GET request
async function ambilPengguna(id: number): Promise<Pengguna> {
  return ambilAPI<Pengguna>(`https://api.example.com/pengguna/${id}`);
}

// POST request
async function buatPengguna(
  data: Omit<Pengguna, "id">
): Promise<Pengguna> {
  return ambilAPI<Pengguna>("https://api.example.com/pengguna", {
    method: "POST",
    body: JSON.stringify(data),
  });
}

// Upload file dengan FormData
async function unggahFile(
  file: File,
  deskripsi: string
): Promise<{ url: string }> {
  const formData = new FormData();
  formData.append("file", file);
  formData.append("deskripsi", deskripsi);

  const response = await fetch("/api/upload", {
    method: "POST",
    body: formData,
    // Jangan set Content-Type — biarkan browser mengaturnya (dengan boundary)
  });

  if (!response.ok) throw new Error(`Upload gagal: ${response.status}`);
  return response.json() as Promise<{ url: string }>;
}

// Fetch dengan timeout
async function ambilDenganTimeout<T>(
  url: string,
  timeoutMs: number
): Promise<T> {
  const controller = new AbortController();
  const timeoutId = setTimeout(() => controller.abort(), timeoutMs);

  try {
    const response = await fetch(url, { signal: controller.signal });
    if (!response.ok) throw new Error(`HTTP ${response.status}`);
    return response.json() as Promise<T>;
  } catch (err) {
    if ((err as Error).name === "AbortError") {
      throw new Error(`Request timeout setelah ${timeoutMs}ms`);
    }
    throw err;
  } finally {
    clearTimeout(timeoutId);
  }
}

File Watching — Mendeteksi Perubahan File #

import { watch } from "fs";
import { stat } from "fs/promises";

// Watch sederhana — deteksi perubahan file
function pantauFile(
  filePath: string,
  callback: (event: string) => void
): () => void {
  const watcher = watch(filePath, { persistent: false }, (event) => {
    callback(event);
  });

  // Kembalikan fungsi untuk menghentikan watching
  return () => watcher.close();
}

// Watch direktori secara rekursif
function pantauDirektori(
  dirPath: string,
  callback: (event: string, filename: string | null) => void
): () => void {
  const watcher = watch(
    dirPath,
    { recursive: true, persistent: false },
    (event, filename) => {
      callback(event, filename);
    }
  );

  return () => watcher.close();
}

// Penggunaan: reload konfigurasi saat file berubah
const hentikanPantauan = pantauFile("konfigurasi.json", async (event) => {
  if (event === "change") {
    console.log("Konfigurasi berubah, memuat ulang...");
    try {
      const konfig = await bacaJSON<Record<string, unknown>>("konfigurasi.json");
      console.log("Konfigurasi baru:", konfig);
    } catch (err) {
      console.error("Gagal memuat konfigurasi baru:", err);
    }
  }
});

// Hentikan watching saat aplikasi ditutup
process.on("SIGINT", () => {
  hentikanPantauan();
  process.exit(0);
});

Pola Alur I/O #

flowchart TD
    A[Operasi I/O] --> B{Ukuran Data?}

    B -- Kecil < 10MB --> C{Jenis?}
    B -- Besar > 10MB --> D[Streaming\ncreateReadStream]

    C -- File teks/JSON --> E[fs/promises\nreadFile writeFile]
    C -- Input pengguna --> F[readline\ntanya-jawab]
    C -- HTTP --> G[fetch API\ntype-safe wrapper]
    C -- File biner --> H[fs/promises\ntanpa encoding]

    D --> I[readline untuk teks\npipeline untuk transform]

    E --> J[Error Handling\nErrnoException code]
    F --> K[Tutup interface\ndi finally]
    G --> L[Cek response.ok\nsebelum .json]

    style D fill:#339af0,color:#fff
    style J fill:#ff6b6b,color:#fff
    style K fill:#fcc419,color:#000
    style L fill:#ff6b6b,color:#fff

Ringkasan #

  • Selalu gunakan async I/O di dalam server atau aplikasi yang melayani banyak pengguna — readFileSync, writeFileSync memblokir event loop dan merusak concurrency; sync hanya untuk script satu-off atau inisialisasi sebelum server dimulai.
  • fs/promises lebih disukai dari fs dengan callback — Promise-based API bekerja langsung dengan async/await tanpa perlu util.promisify, kodenya jauh lebih bersih.
  • Selalu buat direktori sebelum menulis filemkdir(path.dirname(filePath), { recursive: true }) mencegah error ENOENT saat direktori tujuan belum ada.
  • Gunakan streaming untuk file besarcreateReadStream dan readline.Interface memproses file baris per baris tanpa memuat seluruh file ke memori; sangat penting untuk file log, CSV besar, atau data dump.
  • Gunakan path.join() bukan string concatenationpath.join menangani perbedaan separator di Windows (\) dan Unix (/) secara otomatis.
  • Validasi path dari user input — selalu cek bahwa path.resolve(base, userInput).startsWith(path.resolve(base)) untuk mencegah path traversal attack.
  • Cek response.ok sebelum response.json() — fetch tidak melempar error untuk response 4xx/5xx; status error hanya bisa dideteksi dari properti response.ok atau response.status.
  • Gunakan AbortController untuk memberi timeout pada fetch request — tanpanya, request bisa menggantung selamanya jika server tidak merespons.
  • Tangani NodeJS.ErrnoException dengan switch pada properti .codeENOENT (tidak ada file), EACCES (tidak ada izin), EISDIR (target adalah direktori) semua butuh penanganan berbeda.

← Sebelumnya: Multi Threading   Berikutnya: Socket →

About | Author | Content Scope | Editorial Policy | Privacy Policy | Disclaimer | Contact