数字误认作字符,字符串误认作数组,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
为基拓展子类时可能很难得到很好的类型支持; - 每步操作需要在上步操作的返回值上执行,以使用正确完整的类型信息。
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。