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

コレは何
このスレッドを見たのが発端。
少し前に CLI ライブラリであるところの gunshi を使って CLI を作った際に、
設定ファイルのオブジェクトの型と CLI 引数の対応づけを型検査したいと思ってトライしていた。
このスクラップではそれを少し一般化したものをまとめる。

PoC をこちらに用意した。

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

設定オブジェクトから 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[];
// }

実際に実装したユーティリティ型の実装がこちら。

あとはこの型を使って satisfies
で CLI 引数定義に制約をかければ、設定オブジェクトを SSoT として 対応する CLI 引数が定義されていることを型検査レベルで保証できるようになる。

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

設定オブジェクトを 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
})

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

見た感じ行けそうだが、微妙に想定外の使い方になってしまいそう?
シンプルに定義できるのが売りなのに plugin を通さないと引数定義できないものなぁ

schema を渡すと
- Arg object
- Validation する plugin が帰ってきてそれを入れればOK, という helper 運用が無難だろうか
const { args, plugin } = defineArgsByStandardSchema(schema)
と思ったがplugin の追加は global scope, それはそう