博客 / 詳情

返回

TS 分析字符串實現 Commander.js 自動強類型

數字誤認作字符,字符串誤認作數組,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 為基拓展子類時可能很難得到很好的類型支持;
  • 每步操作需要在上步操作的返回值上執行,以使用正確完整的類型信息。
user avatar
0 位用戶收藏了這個故事!

發佈 評論

Some HTML is okay.