YAML di TypeScript #
YAML (YAML Ain’t Markup Language) adalah format serialisasi data yang dirancang untuk mudah dibaca manusia — ia adalah pilihan dominan untuk file konfigurasi, CI/CD pipeline, infrastruktur sebagai kode (Docker Compose, Kubernetes, Ansible, GitHub Actions), dan aplikasi yang perlu konfigurasi yang mudah diedit pengguna. TypeScript tidak memiliki dukungan YAML bawaan seperti JSON, sehingga perlu library pihak ketiga. Yang lebih penting, karena parser YAML mengembalikan unknown atau any, validasi schema dengan Zod sangat kritis untuk mendapatkan type safety. Artikel ini membahas cara bekerja dengan YAML di TypeScript secara aman — dari parsing yang benar, validasi schema, hingga jebakan sintaks YAML yang sering menyebabkan bug tersembunyi.
Sintaks YAML — Dasar dan Jebakan #
Sebelum bekerja dengan YAML di TypeScript, penting memahami sintaks dasarnya dan jebakan yang sering mengejutkan:
# Komentar menggunakan tanda pagar
# --- adalah pemisah dokumen (opsional)
---
# Mapping (seperti object dalam JSON)
server:
host: localhost
port: 5432
ssl: true
# Sequence (seperti array dalam JSON)
databasePool:
- nama: primary
host: db1.example.com
- nama: replica
host: db2.example.com
# String multiline
pesanSelamatDatang: |
Selamat datang di MuslimApps!
Semoga bermanfaat untuk umat.
# String multiline yang di-fold (newline jadi spasi)
deskripsi: >
Aplikasi super untuk
umat Muslim Indonesia.
# Anchor (&) dan alias (*) untuk menghindari duplikasi
defaultLogging: &defaultLogging
level: info
format: json
service1:
logging:
<<: *defaultLogging # Merge dari anchor
level: debug # Override satu nilai
Jebakan: Norway Problem dan Nilai Ambigu #
YAML 1.1 (yang digunakan banyak library lama) memiliki konversi otomatis yang mengejutkan:
# JEBAKAN: Nilai yang diinterpretasikan salah
# Norway problem — kode negara "NO" diinterpretasikan sebagai boolean false!
kodeNegara: NO # false (bukan string "NO"!)
kodeNegara2: "NO" # "NO" yang benar — selalu kutip string ambigu
# Nilai boolean yang mengejutkan di YAML 1.1
aktif1: yes # true
aktif2: on # true
aktif3: y # true
aktif4: no # false
aktif5: off # false
aktif6: n # false
# Angka oktal yang mengejutkan
izinFile: 0755 # 493 (desimal!) bukan string "0755"
izinFile2: "0755" # "0755" string yang benar
# Tanggal yang diparse otomatis
tanggal: 2025-05-07 # Date object, bukan string!
tanggal2: "2025-05-07" # String yang benar jika perlu string
Selalu gunakan tanda kutip ("atau') untuk nilai string yang ambigu di YAML — kode negara dua huruf, nilai boolean-like (yes/no/on/off/true/false), angka dengan leading zero, dan string yang mirip tanggal. Ini mencegah parser menginterpretasikannya sebagai tipe lain secara diam-diam.
Setup: Library YAML untuk TypeScript #
Ada dua library utama untuk YAML di Node.js/TypeScript:
# js-yaml — library paling populer, YAML 1.2
npm install js-yaml
npm install --save-dev @types/js-yaml
# yaml — library modern, lebih lengkap, TypeScript-first
npm install yaml
Perbedaan utama:
| Aspek | js-yaml | yaml |
|---|---|---|
| TypeScript support | Via @types/js-yaml | Native TypeScript |
| YAML version | 1.2 | 1.2 (lebih ketat) |
| API | load, dump | parse, stringify |
| Multi-dokumen | loadAll | parseAllDocuments |
| Custom types | Type class | customTags |
| Bundle size | Lebih kecil | Lebih besar |
Parsing YAML dengan js-yaml
#
import * as yaml from "js-yaml";
import { z } from "zod";
// Parse YAML string ke object
const yamlString = `
server:
host: localhost
port: 3000
ssl: false
database:
url: postgresql://localhost:5432/muslimapps
maxConnections: 10
fitur:
jadwalSholat: true
kiblat: true
zakat: false
`;
// js-yaml mengembalikan unknown — JANGAN cast langsung ke tipe
const rawData = yaml.load(yamlString);
// Tipe: string | number | object | null | undefined
// Validasi dengan Zod untuk type safety
const SchemaKonfigurasi = z.object({
server: z.object({
host: z.string(),
port: z.coerce.number().min(1).max(65535),
ssl: z.boolean().default(false),
}),
database: z.object({
url: z.string().url(),
maxConnections: z.number().int().min(1).max(100).default(10),
}),
fitur: z.record(z.string(), z.boolean()).optional(),
});
type Konfigurasi = z.infer<typeof SchemaKonfigurasi>;
function parseKonfigurasi(yamlInput: string): Konfigurasi {
let raw: unknown;
try {
raw = yaml.load(yamlInput);
} catch (err) {
throw new SyntaxError(
`YAML tidak valid: ${err instanceof Error ? err.message : String(err)}`
);
}
const hasil = SchemaKonfigurasi.safeParse(raw);
if (!hasil.success) {
throw new TypeError(
`Konfigurasi tidak valid:\n${JSON.stringify(hasil.error.flatten().fieldErrors, null, 2)}`
);
}
return hasil.data;
}
const konfig = parseKonfigurasi(yamlString);
console.log(`Server: ${konfig.server.host}:${konfig.server.port}`);
// konfig.server.port → number (bukan string!)
Parsing YAML dengan Library yaml
#
Library yaml menawarkan API yang lebih modern dan kontrol yang lebih halus:
import { parse, stringify, parseAllDocuments } from "yaml";
import { z } from "zod";
// Parse YAML string
const data = parse(`
nama: Budi Santoso
usia: 25
email: [email protected]
`);
// data bertipe any — tetap perlu validasi!
// Stringify: TypeScript → YAML
const objek = {
nama: "MuslimApps",
versi: "2.1.0",
fitur: ["jadwal-sholat", "kiblat", "quran"],
konfigurasi: {
bahasa: "id",
tema: "hijau",
},
};
const yamlOutput = stringify(objek);
console.log(yamlOutput);
// nama: MuslimApps
// versi: 2.1.0
// fitur:
// - jadwal-sholat
// - kiblat
// - quran
// konfigurasi:
// bahasa: id
// tema: hijau
// Opsi stringify
const yamlRapi = stringify(objek, {
indent: 2,
lineWidth: 80,
defaultStringType: "QUOTE_DOUBLE", // Selalu kutip string
});
Membaca File Konfigurasi YAML #
Pola lengkap membaca file YAML yang aman dari file sistem:
import { readFile } from "fs/promises";
import * as yaml from "js-yaml";
import { z } from "zod";
import path from "path";
// Schema untuk file konfigurasi aplikasi
const SchemaAppConfig = z.object({
app: z.object({
nama: z.string(),
versi: z.string().regex(/^\d+\.\d+\.\d+$/, "Format versi harus X.Y.Z"),
env: z.enum(["development", "staging", "production"]).default("development"),
}),
server: z.object({
port: z.coerce.number().default(3000),
host: z.string().default("0.0.0.0"),
corsOrigins: z.array(z.string().url()).default([]),
}),
database: z.object({
host: z.string(),
port: z.coerce.number().default(5432),
nama: z.string(),
poolMin: z.number().int().min(1).default(2),
poolMax: z.number().int().max(50).default(10),
}),
redis: z.object({
host: z.string().default("localhost"),
port: z.coerce.number().default(6379),
ttlDefault: z.number().int().positive().default(3600),
}).optional(),
logging: z.object({
level: z.enum(["debug", "info", "warn", "error"]).default("info"),
format: z.enum(["json", "text"]).default("json"),
}).default({ level: "info", format: "json" }),
});
type AppConfig = z.infer<typeof SchemaAppConfig>;
async function muatKonfigurasi(namaFile = "config.yaml"): Promise<AppConfig> {
const filePath = path.resolve(process.cwd(), "config", namaFile);
let isiFile: string;
try {
isiFile = await readFile(filePath, "utf-8");
} catch (err) {
const error = err as NodeJS.ErrnoException;
if (error.code === "ENOENT") {
throw new Error(`File konfigurasi tidak ditemukan: ${filePath}`);
}
throw new Error(`Gagal membaca file konfigurasi: ${error.message}`);
}
let rawData: unknown;
try {
rawData = yaml.load(isiFile);
} catch (err) {
throw new SyntaxError(
`File ${namaFile} bukan YAML yang valid: ${err instanceof Error ? err.message : String(err)}`
);
}
if (rawData === null || rawData === undefined) {
throw new Error(`File konfigurasi kosong: ${filePath}`);
}
const hasil = SchemaAppConfig.safeParse(rawData);
if (!hasil.success) {
const errors = hasil.error.flatten().fieldErrors;
const pesanError = Object.entries(errors)
.map(([field, msgs]) => ` ${field}: ${(msgs ?? []).join(", ")}`)
.join("\n");
throw new TypeError(`Konfigurasi tidak valid:\n${pesanError}`);
}
return hasil.data;
}
// Contoh file config/config.yaml yang dibaca:
// app:
// nama: MuslimApps
// versi: "2.1.0"
// env: production
// server:
// port: 3000
// corsOrigins:
// - "https://muslimapps.id"
// database:
// host: db.internal
// nama: muslimapps_prod
Multi-Dokumen YAML #
YAML mendukung beberapa dokumen dalam satu file, dipisahkan oleh ---:
import * as yaml from "js-yaml";
// File YAML dengan multiple dokumen
const multiDokumen = `
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: muslimapps-backend
spec:
replicas: 3
---
apiVersion: v1
kind: Service
metadata:
name: muslimapps-backend-svc
spec:
type: ClusterIP
port: 3000
`;
// loadAll dengan callback untuk setiap dokumen
const dokumen: unknown[] = [];
yaml.loadAll(multiDokumen, (doc) => {
dokumen.push(doc);
});
console.log(`Jumlah dokumen: ${dokumen.length}`); // 2
// Atau gunakan library yaml untuk API yang lebih modern
import { parseAllDocuments } from "yaml";
const allDocs = parseAllDocuments(multiDokumen);
const objects = allDocs.map((doc) => doc.toJSON());
Anchor dan Alias — Menghindari Duplikasi #
Anchor (&) dan alias (*) adalah fitur YAML yang memungkinkan reuse nilai tanpa duplikasi:
const yamlDenganAnchor = `
# Definisi anchor
defaultConfig: &default
timeout: 30
retries: 3
logLevel: info
# Penggunaan alias — inherits semua dari &default
serviceA:
<<: *default
port: 3001
serviceB:
<<: *default
port: 3002
logLevel: debug # Override satu nilai
# Anchor untuk string
dbPassword: &dbPass "rahasia123"
primaryDB:
password: *dbPass
replicaDB:
password: *dbPass
`;
const config = yaml.load(yamlDenganAnchor) as Record<string, unknown>;
// Anchor di-resolve saat parsing — hasilnya adalah object biasa
console.log((config.serviceA as Record<string, unknown>).timeout); // 30
console.log((config.serviceB as Record<string, unknown>).logLevel); // "debug"
Serialisasi TypeScript ke YAML #
import { stringify } from "yaml";
interface KonfigurasiDeployment {
namaAplikasi: string;
versi: string;
replicas: number;
environment: Record<string, string>;
healthCheck: {
path: string;
intervalDetik: number;
};
}
const deployment: KonfigurasiDeployment = {
namaAplikasi: "muslimapps-api",
versi: "2.1.0",
replicas: 3,
environment: {
NODE_ENV: "production",
LOG_LEVEL: "info",
},
healthCheck: {
path: "/health",
intervalDetik: 30,
},
};
// Serialisasi ke YAML
const yamlOutput = stringify(deployment, {
indent: 2,
lineWidth: 0, // Jangan wrap baris panjang
});
console.log(yamlOutput);
// namaAplikasi: muslimapps-api
// versi: 2.1.0
// replicas: 3
// environment:
// NODE_ENV: production
// LOG_LEVEL: info
// healthCheck:
// path: /health
// intervalDetik: 30
// Tulis ke file
import { writeFile } from "fs/promises";
await writeFile("deployment.yaml", yamlOutput, "utf-8");
Perbandingan YAML vs JSON vs TOML #
flowchart TD
A{Pilih Format\nKonfigurasi?} --> B{Siapa yang\nmenulis?}
B -- Developer teknis --> C{Perlu\nkomentar?}
B -- End user / non-teknis --> D[YAML\nPaling mudah dibaca]
C -- Ya --> E{Kompleksitas\nstruktur?}
C -- Tidak --> F[JSON\nSimple dan universal]
E -- Sederhana, flat --> G[TOML\nTerstruktur dan eksplisit]
E -- Kompleks, nested --> H[YAML\nFleksibel dan ekspresif]
E -- Dibaca mesin juga --> F
style D fill:#51cf66,color:#fff
style F fill:#339af0,color:#fff
style G fill:#fcc419,color:#000
style H fill:#51cf66,color:#fff| Aspek | YAML | JSON | TOML |
|---|---|---|---|
| Keterbacaan | ✓✓ Sangat mudah | ✓ Cukup | ✓✓ Mudah |
| Komentar | ✓ Ya (#) | ✗ Tidak | ✓ Ya (#) |
| Trailing comma | — | ✗ Tidak | — |
| Tipe data | Lengkap + ambigu | Terbatas tapi jelas | Eksplisit |
| Multi-line string | ✓ Native | ✗ Rumit | ✓ Terbatas |
| Dukungan tool | Sangat luas | Universal | Berkembang |
| Jebakan | Banyak (Norway, dll) | Sedikit | Sedikit |
| Use case | Infra, CI/CD, K8s | API, config sederhana | Cargo, Poetry, Hugo |
Ringkasan #
- Selalu validasi hasil
yaml.load()dengan Zod — sepertiJSON.parse, parser YAML mengembalikanunknownatauany; jangan pernah langsung type assertion tanpa validasi.- Gunakan tanda kutip untuk string ambigu — nilai seperti
NO,yes,on,off,0755, dan tanggal (2025-05-07) bisa diinterpretasikan sebagai boolean, oktal, atau Date oleh parser YAML 1.1; selalu kutip jika bermaksud string.- Library
yamllebih modern darijs-yaml— ia TypeScript-native, mendukung YAML 1.2 yang lebih ketat (tidak ada Norway problem), dan API-nya lebih ekspresif; pertimbangkan migrasi jika memulai proyek baru.- Anchor dan alias untuk menghindari duplikasi dalam file konfigurasi besar —
&namamendefinisikan anchor,*namamerujuknya,<<: *namauntuk merge; dipakai luas di Docker Compose dan Kubernetes.- Multi-dokumen YAML (dipisah
---) berguna untuk bundling beberapa manifest Kubernetes atau konfigurasi yang berkaitan dalam satu file; gunakanloadAll(js-yaml) atauparseAllDocuments(yaml).- YAML untuk konfigurasi manusia, JSON untuk API — YAML lebih nyaman diedit manusia (komentar, tidak perlu kutip semua), JSON lebih baik untuk data exchange antar program (deterministik, universal); pilih berdasarkan siapa yang menulis dan membaca.
- Gagal cepat saat konfigurasi tidak valid — validasi dan parse YAML saat startup aplikasi (
muatKonfigurasi()diindex.ts), bukan di tengah-tengah runtime; error konfigurasi yang ditemukan saat startup jauh lebih mudah dideteksi dan diperbaiki.- Gunakan
z.coerce.number()untuk port dan angka konfigurasi — nilai yang diketik sebagaiport: 3000dalam YAML biasanya sudah number, tapi kadang bisa string tergantung konteks;coercemenangani keduanya dengan aman.