TypeScriptの関数の型引数を相互再帰するパターン
修正前
code:ts
function atomForm<
AtomFields extends Record<string, AtomWithSchema<any>>,
(fields: AtomFields): ...
引数のAtomFieldsに制約をつけるためにgenericsで型引数としていた
しかし、AtomFieldsはそこそこ複雑な型で、atomForm()の使用者がわざわざ指定するようなものではない
atomForm()の利用者目線では、敢えて指定するならValuesに相当する型を指定するのが自然
code:ts
// 指定するならこういう型を指定したい
type Values = { field1: number; field2: string; };
const formAtom = atomForm<Values>({
field1: field1Atom,
field2: field2Atom,
});
ということでそれを型引数に取れるように修正した
1段階目の修正
code:ts
type Values = Record<string, unknown>;
function atomForm<
V extends Values,
Fields extends Record<string, AtomWithSchema<any>> = AtomFields<V>,
(fields: Fields): WritableAtom<AtomFormReturn<V>, V, void> まず、当初の目的の通り1つめの形引数Valuesにする
=でデフォルト値を指定することで利用者は指定を省略できる
以前までFieldsから組み上げていた返り値などの型はValuesから組み上げるように変えた
しかし、これだとValues型の制約が弱いので例えば以下の型のテストに失敗する
code:ts
const formAtom = atomForm({
field1: atomWithSchema<number>(),
field2: atomWithSchema<{ type: number }>(),
});
const { result: form } = renderHook(() => useAtomValue(formAtom));
// ↓これが型エラーになる
expectTypeOf(form.current).toEqualTypeOf<
| {
isValid: false;
}
| {
isValid: true;
values: {
field1: number;
field2: { type: number };
};
}
();
form.values以降の値、例えばform.values.field1などがunknownになってしまう
2段階目の修正
code:ts
function atomForm<
V extends PickValuesFromSchema<Fields>,
Fields extends Record<string, AtomWithSchema<any>> = AtomFields<V>,
(fields: Fields): ...
V側の制約を厳しくした
この時、... extends PickValuesFromSchema<Fields>で第2引数Fieldsを参照している
(第2引数は元々AtomFields<V>で第1引数を参照していた)
2つの型引数が相互に依存する形になったが特に問題なかったのでこれを採用したmrsekut.icon
わざわざ名前をつけるほど頻繁に使わないし、使う場面が来ても自分で再度到達できそうなのであまりメモる必要ないかもしれんがmrsekut.icon
そんなに悩まずスッと書けたのでたぶん将来も書けると思う