头图

数字误认作字符,字符串误认作数组,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 的介绍中有所体现,由于类型定义中每次都是返回一个新实例,

  • CommandOptionArgument 为基拓展子类时可能很难得到很好的类型支持;
  • 每步操作需要在上步操作的返回值上执行,以使用正确完整的类型信息。

华猾稽
1.1k 声望19 粉丝

学生仔