【TypeScript】【型パズル(かも)】カリー化した関数で、1つ目の引数の型に応じて2つ目の引数を動的に変えたい
Q&A
Closed
解決したいこと
下記のような、カリー化された関数を考えています。この関数の1次呼び出し時のパラメータに応じて、その結果得られる戻り値の関数の引数型を変更できるよう、うまい型をあてはめたいと考えてます。
const currying = (obj1: { a?: string }) => (obj2: { a?: (name: string) => string }) => { // ★できるだけ、この行の型定義でどうにかしたい。
if (obj1.a === undefined) {
console.log(obj2.a('test')); // エラーにならないでほしい。最悪、obj2?.aでもよし。curryingの戻り値型が引数に応じてちゃんとしていれば。
} else {
console.log(obj1.a);
}
};
const noArg = currying({}); // 【(obj2: {a: (name: string) => string}) => void】型になってほしい
noArg({}); // 型エラーになってほしい
noArg({ a: (name) => `${name}bar` }); // 'testbar'がコンソール出力されてほしい。
const withArg = currying({ a: 'foo' }); // 【() => void】型になってほしい
withArg({}); // 'foo'がコンソール出力されてほしい。
withArg({ a: (name) => `${name}bar` }); // 'foo'が出力されてほしい。
上記の例では、currying
の型引数が適当なので、currying(***)
と呼び出した後の型は必ず(obj2: {a?: (name: string) => string}) => void
型になってしまいます。そうではなく、以下のようにしたいです。
-
currying({})
の結果は(obj2: {a: (name: string) => string}) => void
型 -
currying({a: 'foo'})
の結果は(obj2: {}) => void
型
自分で試したこと
一応、関数のオーバーロードを使うことで、関数currying
の呼び出し後の型はうまくいきました。ただし、currying
の定義で各引数がanyになってしまい、自分のユースケースだとそれが微妙です。
// 案1 - オーバーロードを使う。
// 自分の実装の都合上、他の引数などでちょっと無理が出てくるため避けたい。
// obj1,obj2がanyになってしまい、この実装外の他のプロパティ定義などがちょっと困る。
type Currying1 = {
(obj1: {}): (obj2: { a: (name: string) => string }) => void;
(obj1: { a: string }): (obj2: {}) => void;
};
const currying1: Currying1 = (obj1: any) => (obj2: any) => {
if (obj1.a === undefined) {
console.log(obj2.a('test'));
} else {
console.log(obj1.a);
}
};
const noArg1 = currying1({}); // 【(obj2: {a: (name: string) => string}) => void】型になってほしい
noArg1({});
noArg1({ a: (name) => `${name}bar` });
const withArg1 = currying1({ a: 'foo' }); // 【() => void】型になってほしい
withArg1({});
withArg1({ a: (name) => `${name}bar` });
オーバーロードの観点よりも、ジェネリクスのなんやかんやでうまい作り方ができないかなぁと考えてます。
背景
※質問に直接関係ないのですが、一応背景を記載しておきます。
React開発で、様々な種類のセレクトボックスを作るため、セレクトボックスの原型を作る部分を共通化したいと考えてます。そこで、以下の様なコードを作成しています。(関数の名前や型定義でやりたいことを察してもらえると助かります。。。)
適当にマスキングしましたが、下記のコードで、generateSelectBoxHooksのgetListがありますが、これを省略可能にして省略した場合は第2引数からリストを直接指定したいと考えてます。
type Args<SelectObject extends any> = { label: string; getList: () => Promise<SelectObject[]> };
const generateSelectBoxHooks =
<SelectObject extends any>(args: Args<SelectObject>) =>
() => {
const { label, getList } = args;
const [list, setList] = useState<SelectObject[]>([]);
useEffect(() => {
let flag = true;
getList().then((l) => flag && setList(l));
return () => {
flag = false;
};
}, []);
const [selected, setSelected] = useState<SelectObject | undefined>(undefined);
const ui = <SelectList label={label} list={list} onSelected={(s) => setSelected(s)} />;
return [ui, selected] as const;
};
// 上記の関数を使って、ユーザー選択用のHooksを作成。
const useUserSelectBox = generateSelectBoxHooks({
label: 'ユーザー',
getList: fetchUsers().then((res) => res.data.users),
});
// 画面でユーザー選択Hooksを使う例。
const PageA = () => {
const [uiUserSelectBox, user] = useUserSelectBox();
const onClick = () => {
alert(user.name);
};
return <>{uiUserSelectBox}</>;
};
下記の様にも使えるように、generateSelectBoxHooksを修正したいです。
const useHogeSelectBox = generateSelectBoxHooks({
label: '何か'
});
const PageB = () => {
const [uiHogeSelectBox, selectedHoge] = useHogeSelectBox({list: ['a', 'b', 'c',]});
return <>{uiHogeSelectBox}</>;
};