Open11

設定オブジェクトの型と CLI 引数の対応づけを TypeScript の型検査レベルで保証してみる

sushichan044sushichan044

コレは何

このスレッドを見たのが発端。

少し前に CLI ライブラリであるところの gunshi を使って CLI を作った際に、
設定ファイルのオブジェクトの型と CLI 引数の対応づけを型検査したいと思ってトライしていた。

このスクラップではそれを少し一般化したものをまとめる。

sushichan044sushichan044

この PoC では、設定オブジェクトの型を SSoT とし、対応する名前の CLI 引数を実装しないと TypeScript の型検査に違反するようにすることを目標とした。
そのため、設定値の型と CLI 引数の値の型の対応までは検査していない。

sushichan044sushichan044

設定オブジェクトから CLI 引数の名前を抽出する

実用性を考慮すると、nest を含むオブジェクトから一意な CLI 引数名を導出する必要がある。
これについては Vitest に着想を得て、 nest したプロパティは dot でつなぐことで解決することにした。

これを型検査に反映するためには、下のように object を変換するユーティリティ型が必要になる。

type Config = {
  root: string;
  featureOne: {
    exclude: string[];
    include: string[];
  };
};

type FlattenConfig = FlattenObject<Config>
// Automatically converts to:
// {
//   "root": string;
//   "featureOne.exclude": string[];
//   "featureOne.include": string[];
// }
sushichan044sushichan044

この方法で解決できないこと

  • discriminated union が絡んでくると、型計算の限界があるため完全な対応づけができない
    • 複雑な設定を回避していただくしかない
sushichan044sushichan044

設定オブジェクトを standard schema 準拠で定義するとして、
standard schema を受け取って対応する arg 定義を返すアダプタとかを書くとよいだろうか。
こういうイメージ

import * as v from "valibot"

const argFoo = v.string()
const argBar = v.string()

const configSchema = v.object({
  foo: argFoo,
  bar: argBar,
})

const cliFlag = toArgsTokens({
  foo: argFoo,
  bar: argBar
})
sushichan044sushichan044

これ要するに standard schema を渡してあげることで、引数の自動定義と handler に渡す前の引数の追加バリデーションができればいいよねという話だな
雰囲気的には middleware とか plugin っぽいので、gunshi plugin とかで行けそうな気がしなくもないんだよな、あとで plugin api を見る

sushichan044sushichan044

schema を渡すと

  • Arg object
  • Validation する plugin が帰ってきてそれを入れればOK, という helper 運用が無難だろうか
const { args, plugin } = defineArgsByStandardSchema(schema)

と思ったがplugin の追加は global scope, それはそう