數字誤認作字元,字串誤認作陣列,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
為基擴充子類時可能很難得到很好的型別支援; - 每步操作需要在上步操作的返回值上執行,以使用正確完整的型別資訊。