Optique: TypeScript के लिए type-safe CLI parser
(optique.dev)नमस्ते! TypeScript में अक्सर CLI टूल बनाते-बनाते मुझे मौजूदा लाइब्रेरीज़ की सीमाएँ खटकती रहीं, इसलिए मैंने एक नया CLI parser बनाया। जिन लोगों की इसमें दिलचस्पी हो सकती है, उनके लिए यह परिचय लिख रहा हूँ.
CLI application विकसित करते समय एक बात हमेशा असुविधाजनक लगी। ज़्यादातर मौजूदा CLI parser libraries configuration object या imperative API से CLI structure को define करती हैं, लेकिन इस तरीके में type safety कमज़ोर हो जाती है और जटिल CLI संरचनाओं को व्यक्त करना भी मुश्किल होता है.
खासकर mutually exclusive option groups को व्यक्त करने के लिए अलग-अलग validation logic को इधर-उधर बिखेरना पड़ता था। “यह option और वह option एक साथ इस्तेमाल नहीं किए जा सकते”, “इस mode में सिर्फ़ ऐसे options ही allowed हैं” जैसी constraints को code में साफ़-सुथरे ढंग से व्यक्त करना आसान नहीं था। और TypeScript इस्तेमाल करने पर भी parsing result का type अक्सर manually define करना पड़ता था.
functional parser combinator नाम का एक approach
इसी वजह से मैंने Haskell के optparse-applicative से प्रेरित होकर functional parser combinator शैली में TypeScript CLI parser बनाया.
मौजूदा तरीका:
// मौजूदा libraries का typical तरीका
const program = new Command()
.option('-p, --port <number>', 'port number')
.option('-h, --host <string>', 'hostname')
.action((options) => {
// options का type any है, या इसे manually define करना पड़ता है
});
Optique का तरीका:
// छोटे parsers को combine करके बड़ा structure बनाना
const serverConfig = object({
port: option("-p", "--port", integer({ min: 1, max: 65535 })),
host: option("-h", "--host", string()),
verbose: option("-v", "--verbose")
});
// TypeScript अपने-आप type infer करता है!
// { port: number, host: string, verbose: boolean }
const config = run(serverConfig);
फ़र्क 1: mutually exclusive options को structure के रूप में व्यक्त करना
सबसे बड़ा फ़र्क यह है कि mutually exclusive option groups को स्वाभाविक रूप से व्यक्त किया जा सकता है। मौजूदा libraries में ऐसी constraints को अलग validation logic से संभालना पड़ता है, लेकिन Optique में or() combinator की मदद से constraint को सीधे structure में समाहित किया जा सकता है.
// server mode vs client mode - पूरी तरह अलग option sets
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 अपने-आप discriminated union बनाता है
// { mode: "server", port: number, workers: number, ssl: boolean } |
// { mode: "client", connect: string, timeout: number, retries: number }
मौजूदा libraries में इस तरह की validation manually करनी पड़ती:
// मौजूदा तरीके की झंझट
if (options.mode === "server" && options.connect) {
throw new Error("--connect को server mode में इस्तेमाल नहीं किया जा सकता");
}
if (options.mode === "client" && options.workers) {
throw new Error("--workers को client mode में इस्तेमाल नहीं किया जा सकता");
}
फ़र्क 2: पूरी तरह automatic type inference
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")
}))
);
// result अपने-आप discriminated union के रूप में infer होता है
const result = run(gitLike);
if (result.type === "add") {
// TypeScript अपने-आप type narrowing कर देता है
console.log(`Adding ${result.files.join(", ")}`);
}
फ़र्क 3: modularity और reusability
merge() combinator की मदद से option groups को reuse किया जा सकता है, इसलिए कई commands में common options को आसानी से share किया जा सकता है.
// reusable option group की definition
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()))
});
// ज़रूरत के हिसाब से combine करना
const devMode = merge(networkOptions, object({ debug: option("--debug") }));
const prodMode = merge(networkOptions, authOptions, loggingOptions);
फ़र्क 4: समृद्ध built-in validation
value parsers सिर्फ़ simple type conversion से आगे बढ़कर meaningful validation भी देते हैं.
const parser = object({
// file system में वास्तविक मौजूदगी की जाँच
inputFile: option("--input", path({ mustExist: true })),
// port number range validation
port: option("-p", "--port", integer({ min: 1, max: 65535 })),
// URL protocol restriction
api: option("--api", url({ allowedProtocols: ["https:"] })),
// choices restriction
logLevel: option("--log", choice(["debug", "info", "warn", "error"]))
});
runtime support
- @optique/core: सभी JavaScript runtimes का समर्थन (browser, edge functions आदि)
- @optique/run: Node.js, Bun, Deno के लिए batteries-included version
इंस्टॉलेशन:
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
अंत में
अगर मौजूदा CLI libraries “configuration के जरिए parser बनाने” के तरीके पर आधारित हैं, तो Optique “छोटे parsers को जोड़कर बड़ा parser बनाने” वाला एक functional approach है.
खासकर mutually exclusive option groups को व्यक्त करते समय यह फ़र्क साफ़ दिखाई देता है। जटिल CLI constraints को अलग validation logic के बिना parser structure में ही व्यक्त किया जा सकता है, इसलिए type safety और code simplicity दोनों एक साथ मिलती हैं.
बेशक यह अभी शुरुआती development stage में है, इसलिए API बदल सकता है। लेकिन अगर आप functional parser combinator की elegance को TypeScript CLI development में लाना चाहते हैं, तो इसे एक बार आज़माना अच्छा रहेगा.
- GitHub: https://github.com/dahlia/optique
- दस्तावेज़: https://optique.dev/
- npm: https://www.npmjs.com/package/@optique/core
- JSR: https://jsr.io/@optique/core
1 टिप्पणियां
वाह, काफ़ी अच्छा है! शेयर करने के लिए धन्यवाद। मुझे भी CLI बनाते समय इसे आज़माना पड़ेगा।