Optique: Trình phân tích CLI an toàn kiểu cho TypeScript
(optique.dev)Xin chào! Vì tôi thường xuyên tạo các công cụ CLI bằng TypeScript và cảm thấy tiếc nuối trước những giới hạn của các thư viện hiện có, nên tôi đã làm ra một trình phân tích CLI mới. Tôi muốn giới thiệu nó tới những ai có thể quan tâm.
Trong quá trình phát triển ứng dụng CLI, luôn có một điểm khiến tôi thấy bất tiện. Phần lớn các thư viện phân tích CLI hiện nay định nghĩa cấu trúc CLI bằng đối tượng cấu hình hoặc API mệnh lệnh, nhưng cách này không chỉ thiếu an toàn kiểu mà còn khó biểu diễn các cấu trúc CLI phức tạp.
Đặc biệt, để biểu diễn các nhóm tùy chọn loại trừ lẫn nhau (mutually exclusive), bạn thường phải rải logic kiểm tra riêng ở nhiều nơi. Những ràng buộc như “không thể dùng tùy chọn này cùng với tùy chọn kia” hoặc “ở chế độ này chỉ cho phép các tùy chọn như thế này” rất khó biểu diễn gọn gàng bằng mã. Và dù dùng TypeScript, trong nhiều trường hợp bạn vẫn phải tự định nghĩa kiểu của kết quả phân tích.
Cách tiếp cận bằng parser combinator hàm
Vì vậy, lấy cảm hứng từ optparse-applicative của Haskell, tôi đã thử tạo một trình phân tích CLI cho TypeScript theo phong cách parser combinator hàm.
Cách làm truyền thống:
// Cách điển hình của các thư viện hiện có
const program = new Command()
.option('-p, --port <number>', 'port number')
.option('-h, --host <string>', 'hostname')
.action((options) => {
// kiểu của options là any hoặc phải tự định nghĩa thủ công
});
Cách của Optique:
// Ghép các parser nhỏ để tạo thành cấu trúc lớn
const serverConfig = object({
port: option("-p", "--port", integer({ min: 1, max: 65535 })),
host: option("-h", "--host", string()),
verbose: option("-v", "--verbose")
});
// TypeScript tự động suy luận kiểu!
// { port: number, host: string, verbose: boolean }
const config = run(serverConfig);
Điểm khác biệt 1: Biểu diễn các tùy chọn loại trừ lẫn nhau bằng chính cấu trúc
Điểm khác biệt lớn nhất là có thể biểu diễn tự nhiên các nhóm tùy chọn loại trừ lẫn nhau. Với các thư viện hiện có, những ràng buộc như vậy phải được xử lý bằng logic kiểm tra riêng, còn Optique có thể nhúng trực tiếp ràng buộc vào chính cấu trúc thông qua combinator or().
// Chế độ server vs chế độ client - bộ tùy chọn hoàn toàn khác nhau
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 tự động tạo discriminated union
// { mode: "server", port: number, workers: number, ssl: boolean } |
// { mode: "client", connect: string, timeout: number, retries: number }
Với các thư viện hiện có, bạn hẳn sẽ phải tự viết phần kiểm tra như sau:
// Sự rườm rà của cách làm cũ
if (options.mode === "server" && options.connect) {
throw new Error("--connect không thể dùng trong chế độ server");
}
if (options.mode === "client" && options.workers) {
throw new Error("--workers không thể dùng trong chế độ client");
}
Điểm khác biệt 2: Suy luận kiểu hoàn toàn tự động
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")
}))
);
// Kết quả được tự động suy luận thành discriminated union
const result = run(gitLike);
if (result.type === "add") {
// TypeScript tự động thu hẹp kiểu
console.log(`Adding ${result.files.join(", ")}`);
}
Điểm khác biệt 3: Tính mô-đun và khả năng tái sử dụng
Combinator merge() cho phép tái sử dụng các nhóm tùy chọn, nên bạn có thể dễ dàng chia sẻ các tùy chọn dùng chung giữa nhiều lệnh.
// Định nghĩa nhóm tùy chọn có thể tái sử dụng
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()))
});
// Kết hợp theo nhu cầu
const devMode = merge(networkOptions, object({ debug: option("--debug") }));
const prodMode = merge(networkOptions, authOptions, loggingOptions);
Điểm khác biệt 4: Xác thực tích hợp phong phú
Các parser giá trị không chỉ dừng ở việc chuyển đổi kiểu đơn giản mà còn cung cấp các kiểm tra có ý nghĩa.
const parser = object({
// Kiểm tra xem có thực sự tồn tại trong hệ thống tệp hay không
inputFile: option("--input", path({ mustExist: true })),
// Kiểm tra phạm vi số cổng
port: option("-p", "--port", integer({ min: 1, max: 65535 })),
// Giới hạn giao thức URL
api: option("--api", url({ allowedProtocols: ["https:"] })),
// Giới hạn lựa chọn
logLevel: option("--log", choice(["debug", "info", "warn", "error"]))
});
Hỗ trợ runtime
- @optique/core: hỗ trợ mọi runtime JavaScript (trình duyệt, edge function, v.v.)
- @optique/run: phiên bản kèm đầy đủ tiện ích cho Node.js, Bun, Deno
Cài đặt:
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
Lời kết
Nếu các thư viện CLI hiện có đi theo hướng “tạo parser thông qua cấu hình”, thì Optique là cách tiếp cận hàm theo kiểu “ghép các parser nhỏ để tạo thành parser lớn”.
Sự khác biệt này thể hiện rất rõ khi biểu diễn các nhóm tùy chọn loại trừ lẫn nhau. Bạn có thể mô tả các ràng buộc CLI phức tạp ngay trong chính cấu trúc parser mà không cần logic kiểm tra riêng, từ đó đồng thời đạt được tính an toàn kiểu và sự gọn gàng của mã.
Tất nhiên dự án vẫn đang ở giai đoạn phát triển ban đầu nên API có thể thay đổi, nhưng nếu bạn muốn mang sự thanh lịch của parser combinator hàm vào phát triển CLI với TypeScript thì rất đáng thử một lần.
- GitHub: https://github.com/dahlia/optique
- Tài liệu: https://optique.dev/
- npm: https://www.npmjs.com/package/@optique/core
- JSR: https://jsr.io/@optique/core
1 bình luận
Wow, hay quá! Cảm ơn vì đã chia sẻ. Lúc mình làm CLI cũng phải thử dùng mới được.