charon1212
@charon1212

Are you sure you want to delete the question?

Leaving a resolved question undeleted may help others!

【TypeScript】【型パズル(かも)】カリー化した関数で、1つ目の引数の型に応じて2つ目の引数を動的に変えたい

解決したいこと

下記のような、カリー化された関数を考えています。この関数の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}</>;
};
0

4Answer

Comments

  1. @charon1212

    Questioner

    ご回答いただき、ありがとうございます。
    ご提示いただいた通りで、Conditional Typesを使ってうまいことやりたいと考えているのですが、
    背景にも書いたような複雑な状況だと"引数がどのように指定されているか"をGeneric Typeで引き上げて、それをConditional Typesでうまい分岐をする方法が分からず質問させていただいてます。

Currying1の発想はとてもいいので、obj1を{a?: string}、をunknownで受け取ってはどうでしょうか

1Like

Comments

  1. @charon1212

    Questioner

    ご回答いただき、ありがとうございます。
    オーバーロードは割と苦肉の策で、あまりTypeScriptと相性の良い解決策だと思ってなかったのですが、やっぱりこれしかないですかね…
  2. 私的には、コードの見やすさと実装の簡単さ、unsafeの除去を鑑みるに、1番かなと思います!

    ちなみにanyは悪手なので、ギリギリまで使わない方が安全です💦
1Like

Comments

  1. @charon1212

    Questioner

    ご回答いただき、ありがとうございます。
    具体的な実装まで載せて頂けて、分かりやすかったです。

    頂いた実装をもとに背景側にのせてあるコードをいろいろいじっていたのですが、カリー化をもう1階層深くすることでうまいこと実装できそうです。
    ありがとうございました。

    (動作確認中にmaterial-uiの別問題にぶつかって、しばらく詰まってました…)

@chocolamint さんから頂いた回答をもとに、背景に記載していたコードをどう変更したかを一応コメントに記載いたします。

(tsxのハイライトがうまくいかないので、VSCodeとかに張り付けてみてください。)

/** Qiita回答用 */

/** 適当なMock */
// import { useEffect, useState } from 'react';
const fetchUsers = () => new Promise<{ data: { users: { name: string; age: number }[] } }>((resolve) => resolve({ data: { users: [] } }));
const SelectList = <T extends any>(prop: { label: string; list: T[]; onSelected: (t: T) => void }) => <>Mock</>;

type Args<SelectObject extends any> = { label: string };
type GetList<SelectObject extends any> = () => Promise<SelectObject[]>;
const generateSelectBoxHooks =
  <SelectObject extends any>(args: Args<SelectObject>) =>
  <Args2 extends { getList?: GetList<SelectObject> }>(args2: Args2) =>
  (args3: Args2 extends { getList: GetList<SelectObject> } ? { list?: never } : { list: SelectObject[] }) => {
    const { label } = args;
    const { getList } = args2;
    const inputList = args3.list;
    const [list, setList] = useState<SelectObject[]>([]);

    // 初期値の場合
    useEffect(() => {
      setList([...inputList]);
    }, [inputList]);
    // fetchの場合
    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を作成。
// Hooks1: リストは適当なfetch関数で取得する。
const useUserSelectBox1 = generateSelectBoxHooks<{ name: string; age: number }>({
  label: 'ユーザー',
})({ getList: () => fetchUsers().then((res) => res.data.users) });
// Hooks2: リストはHooks呼び出し時に指定する。
const useUserSelectBox2 = generateSelectBoxHooks<{ name: string; age: number }>({
  label: 'ユーザー',
})({});

// 画面でユーザー選択Hooksを使う例。 - ユーザー一覧はHooks側でfetchしてもらう版(Hooks1)
const PageA = () => {
  const [uiUserSelectBox, user] = useUserSelectBox1({});
  // const [uiUserSelectBox, user] = useUserSelectBox1({ list: [{ name: '山田太郎', age: 10 }, { name: '佐藤次郎', age: 20 }] }); // <<< type error >>>
  const onClick = () => {
    alert(user?.name);
  };
  return <>{uiUserSelectBox}</>;
};

// 画面でユーザー選択Hooksを使う例。 - ユーザー一覧は決まったリストを出してもらう版(Hooks2)
const PageB = () => {
  // const [uiUserSelectBox, user] = useUserSelectBox2({  }); // <<< type error >>>
  const [uiUserSelectBox, user] = useUserSelectBox2({
    list: [
      { name: '山田太郎', age: 10 },
      { name: '佐藤次郎', age: 20 },
    ],
  });
  const onClick = () => {
    alert(user?.name);
  };
  return <>{uiUserSelectBox}</>;
};

0Like

Your answer might help someone💌