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 request — readFileSync, 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<br/>createReadStream"]

    C -->|File teks/JSON| E["fs/promises<br/>readFile writeFile"]
    C -->|Input pengguna| F["readline<br/>tanya-jawab"]
    C -->|HTTP| G["fetch API<br/>type-safe wrapper"]
    C -->|File biner| H["fs/promises<br/>tanpa encoding"]

    D --> I["readline untuk teks<br/>pipeline untuk transform"]

    E --> J["Error Handling<br/>ErrnoException code"]
    F --> K["Tutup interface<br/>di finally"]
    G --> L["Cek response.ok<br/>sebelum .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 file — mkdir(path.dirname(filePath), { recursive: true }) mencegah error ENOENT saat direktori tujuan belum ada.
  • Gunakan streaming untuk file besar — createReadStream 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 concatenation — path.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 .code — ENOENT (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