數字誤認作字符,字符串誤認作數組,Promise 沒有 await 就取值,這些問題在 TypeScript 裏把每個類型都定義對了就不會出現,還會有很好的編輯提示。
但寫命令行工具,定義一個某類型的選項時,一邊要傳參如 .option("-d, --dev"),一邊要標註類型如 { dev: boolean },兩個地方需要手動同步。繁瑣易錯,怎麼辦?TypeScript 早在 4.1 就可以設計分析字符串生成類型了。
現在,通過 @commander-js/extra-typings 就可以自動得到字符串中設計的命令結構。
import { program } from '@commander-js/extra-typings';
program
.argument("<input>")
.argument("[outdir]")
.option("-c, --camel-case")
.action((input, outputDir, options) => {
// input 是 string
// outputDir 是 string | undefined
// options 是 { camelCase?: true | undefined }
});
本文介紹 @commander-js/extra-typings 用到的關鍵技術。
必需 / 可選,單個 / 數個
必須 / 可選參數 往往形如 <xxx> / [xxx],其中 xxx 為參數名。
參數名以 ... 結尾時,表示該參數可以包含多個取值。
對於這樣的字符串,使用 extends 關鍵字即可設計條件對應類型。
// S 取 "<arg>" 得 true
// S 取 "[arg]" 得 false
type IsRequired<S extends string> =
S extends `<${string}>` ? true : false;
// S 取 "<arg...>" 得 true
// S 取 "<arg>" 得 false
type IsVariadic<S extends string> =
S extends `${string}...${string}` ? true : false;
選項名
選項名時常有精簡寫法,如 -r 可能表示 --recursive。作為命令行選項時通常使用 - 配合小寫字母的命名方式,在代碼中則常用駝峯命名法。
對於使用 逗號+空格 來提前放置精簡寫法的選項,可以使用 infer 關鍵字推導模板文字遞歸化簡。
// S 取 "-o, --option-name" 得 "option-name"
type OptionName<S extends string> =
S extends `${string}, ${infer R}`
? OptionName<R> // 去除逗號,空格,及之前的內容
: S extends `-${infer R}`
? OptionName<R> // 去除開頭的 "-"
: S;
將短線 - 轉換為駝峯命名,可以結合 Capitalize。
// S 取 "option-name" 得 "optionName"
type CamelCase<S extends string> =
S extends `${infer W}-${infer R}`
? CamelCase<`${W}${Capitalize<R>}`>
: S;
變長參數
參數長度不定的函數,參數可以通過展開類型元組來定義類型。
type Args = [boolean, string, number];
type VarArgFunc = (...args: Args) => void;
const func: VarArgFunc = (arg1, arg2, arg3) => {
// arg1 為 boolean
// arg2 為 string
// arg3 為 number
};
類型元組可以儲存在類參數中,並同樣通過展開運算符 ... 來結合新元素。
declare class Foo<Args extends unknown[] = []> {
concat<T>(arg: T): Foo<[...Args, T]>;
run(fn: (...args: Args) => void): void;
}
const foo = new Foo()
.concat(1)
.concat("str")
.concat(true);
foo.run((arg1, arg2, arg3) => {
// arg1 為 number
// arg2 為 string
// arg3 為 boolean
});
限制
實現 @commander-js/extra-typings 遇到的最大障礙,在於對 this 信息的保留。在變長參數一節,每次 concat 添加信息都需要返回一個新實例,能不能使用 & 或 mixin 等其他技術結合 this 呢?目前實測結果是 不能,TS 在這類實測中,非常容易報錯或卡死,不卡死時在某些地方會提示 TS 檢查陷入死循環,不卡死不報錯時往往是陷入了無響應的狀態。
相關記錄可以在原實現 PR #1758 · tj/commander.js 中找到。
這樣的限制也在 @commander-js/extra-typings 的介紹中有所體現,由於類型定義中每次都是返回一個新實例,
- 以
Command、Option、Argument為基拓展子類時可能很難得到很好的類型支持; - 每步操作需要在上步操作的返回值上執行,以使用正確完整的類型信息。