Optique: parser CLI yang aman tipe untuk TypeScript
(optique.dev)Halo! Karena saya cukup sering membuat alat CLI dengan TypeScript, saya merasa keterbatasan pustaka-pustaka yang ada cukup disayangkan, jadi saya pun membuat parser CLI baru. Saya ingin memperkenalkannya kepada siapa pun yang mungkin tertarik.
Saat mengembangkan aplikasi CLI, ada satu hal yang selalu terasa tidak nyaman. Kebanyakan pustaka parser CLI yang ada mendefinisikan struktur CLI dengan objek konfigurasi atau API imperatif, tetapi pendekatan seperti ini bukan hanya kurang aman dari sisi tipe, melainkan juga menyulitkan untuk mengekspresikan struktur CLI yang kompleks.
Terutama, untuk mengekspresikan grup opsi yang saling eksklusif (mutually exclusive), kita harus menyebarkan logika validasi terpisah ke berbagai tempat. Batasan seperti “opsi ini dan opsi itu tidak bisa dipakai bersamaan” atau “dalam mode ini hanya opsi seperti ini yang diizinkan” sulit dinyatakan dengan rapi di dalam kode. Dan meskipun memakai TypeScript, dalam banyak kasus kita tetap harus mendefinisikan tipe hasil parsing secara manual.
Pendekatan yang disebut parser combinator fungsional
Karena itu, terinspirasi oleh optparse-applicative dari Haskell, saya mencoba membuat parser CLI TypeScript dengan pendekatan parser combinator fungsional.
Pendekatan lama:
// Cara yang umum pada pustaka yang ada
const program = new Command()
.option('-p, --port <number>', 'nomor port')
.option('-h, --host <string>', 'nama host')
.action((options) => {
// tipe options adalah any atau harus didefinisikan manual
});
Pendekatan Optique:
// Menggabungkan parser-parser kecil untuk membentuk struktur yang besar
const serverConfig = object({
port: option("-p", "--port", integer({ min: 1, max: 65535 })),
host: option("-h", "--host", string()),
verbose: option("-v", "--verbose")
});
// TypeScript otomatis menginferensikan tipe!
// { port: number, host: string, verbose: boolean }
const config = run(serverConfig);
Pembeda 1: mengekspresikan opsi yang saling eksklusif sebagai struktur
Pembeda terbesar adalah bahwa grup opsi yang saling eksklusif bisa diekspresikan secara alami. Pustaka yang ada biasanya harus menangani batasan seperti ini dengan logika validasi terpisah, tetapi Optique dapat melekatkan batasan tersebut langsung ke dalam strukturnya dengan kombinator or().
// Mode server vs mode client - set opsi yang benar-benar berbeda
const parser = or(
object({
mode: constant("server"),
port: option("-p", "--port", integer()),
workers: option("-w", "--workers", integer()),
ssl: option("--ssl")
}),
object({
mode: constant("client"),
connect: option("-c", "--connect", string()),
timeout: option("-t", "--timeout", integer()),
retries: option("--retries", integer())
})
);
// TypeScript otomatis membuat discriminated union
// { mode: "server", port: number, workers: number, ssl: boolean } |
// { mode: "client", connect: string, timeout: number, retries: number }
Pada pustaka yang ada, validasi seperti ini kemungkinan harus dilakukan manual:
// Kerumitan pendekatan lama
if (options.mode === "server" && options.connect) {
throw new Error("--connect tidak bisa digunakan dalam mode server");
}
if (options.mode === "client" && options.workers) {
throw new Error("--workers tidak bisa digunakan dalam mode client");
}
Pembeda 2: inferensi tipe sepenuhnya otomatis
const gitLike = or(
command("add", object({
type: constant("add"),
files: multiple(argument(string())),
all: option("-A", "--all")
})),
command("commit", object({
type: constant("commit"),
message: option("-m", "--message", string()),
amend: option("--amend")
}))
);
// Hasilnya otomatis diinferensikan sebagai discriminated union
const result = run(gitLike);
if (result.type === "add") {
// TypeScript akan melakukan type narrowing secara otomatis
console.log(`Adding ${result.files.join(", ")}`);
}
Pembeda 3: modularitas dan dapat digunakan ulang
Dengan kombinator merge(), grup opsi dapat digunakan ulang, sehingga opsi bersama bisa dibagikan dengan mudah di beberapa perintah.
// Mendefinisikan grup opsi yang bisa digunakan ulang
const networkOptions = object({
host: option("--host", string()),
port: option("--port", integer())
});
const authOptions = object({
username: option("-u", "--user", string()),
password: optional(option("-p", "--password", string()))
});
// Menggabungkannya sesuai kebutuhan
const devMode = merge(networkOptions, object({ debug: option("--debug") }));
const prodMode = merge(networkOptions, authOptions, loggingOptions);
Pembeda 4: validasi bawaan yang kaya
Parser nilai tidak hanya melakukan konversi tipe sederhana, tetapi juga menyediakan validasi yang bermakna.
const parser = object({
// Memeriksa apakah benar-benar ada di file system
inputFile: option("--input", path({ mustExist: true })),
// Validasi rentang nomor port
port: option("-p", "--port", integer({ min: 1, max: 65535 })),
// Membatasi protokol URL
api: option("--api", url({ allowedProtocols: ["https:"] })),
// Membatasi pilihan
logLevel: option("--log", choice(["debug", "info", "warn", "error"]))
});
Dukungan runtime
- @optique/core: mendukung semua runtime JavaScript (browser, edge function, dll.)
- @optique/run: versi battery-included untuk Node.js, Bun, dan Deno
Instalasi:
deno add --jsr @optique/core @optique/run
npm add @optique/core @optique/run
pnpm add @optique/core @optique/run
yarn add @optique/core @optique/run
bun add @optique/core @optique/run
Penutup
Jika pustaka CLI yang ada menggunakan pendekatan “membuat parser melalui konfigurasi”, maka Optique memakai pendekatan fungsional “membuat parser besar dengan menggabungkan parser-parser kecil”.
Perbedaan ini особенно terlihat saat mengekspresikan grup opsi yang saling eksklusif. Batasan CLI yang kompleks bisa diekspresikan langsung lewat struktur parser itu sendiri tanpa logika validasi terpisah, sehingga kita bisa mendapatkan keamanan tipe dan keringkasan kode sekaligus.
Tentu saja, karena masih berada pada tahap pengembangan awal, API-nya bisa berubah. Namun, bagi siapa pun yang ingin membawa keanggunan parser combinator fungsional ke pengembangan CLI TypeScript, sepertinya layak dicoba.
- GitHub: https://github.com/dahlia/optique
- Dokumentasi: https://optique.dev/
- npm: https://www.npmjs.com/package/@optique/core
- JSR: https://jsr.io/@optique/core
1 komentar
Wah, bagus sekali! Terima kasih sudah berbagi. Saya juga sepertinya harus mencobanya saat membuat CLI.