2
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

【TypeScript】コールバック関数の型設計について

Posted at

はじめに

Reactのコンポーネント作成の際に、propsとして受け取る関数の型設計につまずいたため、自分なりの理解をまとめておきます。
関数型の部分型関係の話にもなるので内容は難しめですが、参考になれば幸いです。

結論

コールバック関数を受け取る側について、コールバック関数の

  • 返り値の型は広いほうが良い!(コールバック関数を広く受け入れる)
  • 引数の型は狭いほうがいい!(渡される引数の型を限定してあげる)
  • 引数の数は多いほうが良い(多すぎるのも使いづらくなるが)

コールバック関数を渡す側の視点だと、

  • 返り値の型は、受け取る側で指定されている型と同じか狭くないといけない
  • 引数の型は、受け取る側で指定されている型と同じか広くないといけない
  • 引数の数は、受け取る側で指定されている数と同じか少ないといけない

サンプルコードの紹介

まず、doSomethingWithKeyAndValue関数を用意しています。
この関数は、内部的にdatakeyvalueというプロパティを持つオブジェクトの配列)を持っています。
dataの各要素のkeyvalueに対して、コールバック関数を実行させ、各要素の実行結果を配列にして返す関数です。
また、コールバック関数の引数は、基本的にkeyvalueです。

以下の例では、doSomethingWithKeyAndValue関数に、myFnというコールバック関数を渡しています。
この関数myFnは、keyvalueを半角スペースで結合する関数です。
そのため、doSomethingWithKeyAndValue関数の返り値は、keyvalueが半角スペースで結合された要素からなる配列を返します。

コールバック関数を受け取る側
const doSomethingWithKeyAndValue = (
  callbackfn: (key: string, value: string) => string
): Array<string> => {

  const data = [
    { key: "red", value: "" },
    { key: "blue", value: "" },
    { key: "gray", value: "" },
  ];
  return data.map((item) => {
    return callbackfn(item.key, item.value); // ここでコールバック関数を実行
  });
};
コールバック関数を渡す側
// key と value を半角スペースで連結する関数
const myFn = (key: string, value: string): string => {
  return key + " " + value;
};

console.log(doSomethingWithKeyAndValue(myFn));
実行結果
["red 赤", "blue 青", "gray 灰"]

また、上記の例の場合、

コールバック関数を受け取る側の型は(key: string, value: string) => string
実際に渡しているコールバック関数の型も(key: string, value: string) => string

になります。

コールバック関数の返り値の型

まずは、コールバック関数を受け取る側と渡す側の、コールバック関数の返り値の型に違いがある場合を検証していきます。

コールバック関数を受け取るほうが、返り値の型の範囲が広い場合✅

以下のコードは、

  • コールバック関数を受け取る側の型は(key: string, value: string) => string | number
  • 実際に渡しているコールバック関数の型は(key: string, value: string) => string

となっています。

返り値の型に注目すると

  • コールバック関数を受け取る側の返り値の型はstring | number
  • 実際に渡しているコールバック関数の返り値の型はstring

となります。

このコードは問題ないです。

コールバック関数を受け取る側
const doSomethingWithKeyAndValue = (
  callbackfn: (key: string, value: string) => string | number
): Array<string | number> => {

  const data = [
    { key: "red", value: "" },
    { key: "blue", value: "" },
    { key: "gray", value: "" },
  ];
  return data.map((item) => {
    return callbackfn(item.key, item.value); // ここでコールバック関数を実行
  });
};
コールバック関数を渡す側
const myFn = (key: string, value: string): string => {
  return key + " " + value;
};

console.log(doSomethingWithKeyAndValue(myFn));
実行結果
["red 赤", "blue 青", "gray 灰"]

コールバック関数を受け取る側は、コールバック関数の返り値がstring型かnumber型であることを想定しているのに対し、
実際に渡されるコールバック関数myFnの返り値はstring型なので、
コールバック関数を受け入れ可能となります。

コールバック関数を受け取るのほうが、返り値の型の範囲が狭い場合❌

以下のコードは、

コールバック関数を受け取る側の型は(key: string, value: string) => string
実際に渡しているコールバック関数の型は(key: string, value: string) => string | number

となっています。

返り値の型に注目すると

  • コールバック関数を受け取る側の返り値の型はstring
  • 実際に渡しているコールバック関数の返り値の型はstring | number

となります。

このコードの場合、doSomethingWithKeyAndValue関数の実行部分でコンパイルエラーとなります。

コールバック関数を受け取る側
const doSomethingWithKeyAndValue = (
  callbackfn: (key: string, value: string) => string
): Array<string> => {
  
  const data = [
    { key: "red", value: "" },
    { key: "blue", value: "" },
    { key: "gray", value: "" },
  ];
  return data.map((item) => {
    return callbackfn(item.key, item.value); // ここでコールバック関数を実行
  });
};
コールバック関数を渡す側
const myFn = (key: string, value: string): string | number => {
  if (key === "gray") {
    return 808080;
  }
  return key + " " + value;
}

console.log(doSomethingWithKeyAndValue(myFn)); // myFnでコンパイルエラー!

型 '(key: string, value: string) => string | number' の引数を型 '(key: string, value: string) => string' のパラメーターに割り当てることはできません。
型 'string | number' を型 'string' に割り当てることはできません。
型 'number' を型 'string' に割り当てることはできません。

実行結果
["red 赤", "blue 青", 808080]

コールバック関数を受け取る側は、コールバック関数の返り値がstring型であることを想定しているのに対し、
実際に渡されるコールバック関数myFnstring型以外にもnumber型を返す可能性があるため、
コールバック関数を受け入れることができずコンパイルエラーとなります。

(実行自体はできますが)

まとめ

コールバック関数を受け取る側について、コールバック関数の返り値の型は広いほうが良い!(コールバック関数を広く受け入れる)

コールバック関数の引数の型

次に、コールバック関数を受け取る側と渡す側の、コールバック関数の引数の型に違いがある場合を検証していきます。

コールバック関数を受け取るほうが、引数の型の範囲が広い場合❌

以下のコードは、

コールバック関数を受け取る側の型は(key: string, value: string | number) => string
実際に渡しているコールバック関数の型は(key: string, value: string) => string

となっています。

引数valueの型に注目すると

  • コールバック関数を受け取る側の引数valueの型はstring | number
  • 実際に渡しているコールバック関数の引数valueの型はstring

となります。

このコードの場合、doSomethingWithKeyAndValue関数の実行部分でコンパイルエラーとなります。

コールバック関数を受け取る側
const doSomethingWithKeyAndValue = (
  callbackfn: (key: string, value: string | number) => string
): Array<string> => {

  const data = [
    { key: "red", value: "" },
    { key: "blue", value: "" },
    { key: "gray", value: 808080 },
  ];
  return data.map((item) => {
    return callbackfn(item.key, item.value); // ここでコールバック関数を実行
  });
};
コールバック関数を渡す側
const myFn = (key: string, value: string): string => {
  return key + " " + value;
};

console.log(doSomethingWithKeyAndValue(myFn)); // myFnでコンパイルエラー

型 '(key: string, value: string) => string' の引数を型 '(key: string, value: string | number) => string' のパラメーターに割り当てることはできません。
パラメーター 'value' および 'value' は型に互換性がありません。
型 'string | number' を型 'string' に割り当てることはできません。
型 'number' を型 'string' に割り当てることはできません。

実行結果
["red 赤", "blue 青", "gray 808080"]

コールバック関数を受け取る側は、コールバック関数の引数valuestring型かnumber型の値を渡しますが、
実際に渡されるコールバック関数myFnの引数valuestring型が渡されることしか考慮されていないため、
コールバック関数を受け入れることができずコンパイルエラーとなります。

(TypeScriptで文字列と数字を+演算子で結合すると、数字が文字列に変換され、文字列として連結されるため、実行自体はできますが)

コールバック関数を受け取るほうが、引数の型の範囲が狭い場合✅

以下のコードは、

コールバック関数を受け取る側の型は(key: string, value: string) => string
実際に渡しているコールバック関数の型は(key: string, value: string | number) => string

となっています。

引数valueの型に注目すると

  • コールバック関数を受け取る側の引数valueの型はstring
  • 実際に渡しているコールバック関数の引数valueの型はstring | number

となります。

このコードは問題ないです。

コールバック関数を受け取る側
const doSomethingWithKeyAndValue = (
  callbackfn: (key: string, value: string) => string
): Array<string> => {

  const data = [
    { key: "red", value: "" },
    { key: "blue", value: "" },
    { key: "gray", value: "" },
  ];
  return data.map((item) => {
    return callbackfn(item.key, item.value); // ここでコールバック関数を実行
  });
};
コールバック関数を渡す側
const myFn = (key: string, value: string | number): string => {
  return key + " " + String(value);
};

console.log(doSomethingWithKeyAndValue(myFn));
実行結果
["red 赤", "blue 青", "gray 灰"]

コールバック関数を受け取る側は、コールバック関数の引数valuestring型の値を渡し、
実際に渡されるコールバック関数myFnの引数valuestring型かnumber型の値を渡せるので、
コールバック関数を受け入れ可能となります。

まとめ

コールバック関数を受け取る側について、コールバック関数の引数の型は狭いほうがいい!(渡される引数の型を限定してあげる)

コールバック関数の引数の数

最後に、コールバック関数を受け取る側と渡す側の、コールバック関数の引数の数に違いがある場合を検証していきます。

コールバック関数を受け取るほうが、引数の数が多い場合✅

以下のコードは、

コールバック関数の型は(key: string, value: string, code: string) => string
実際に渡している関数の型は(key: string, value: string) => string

となっています。

引数の数に注目すると

  • コールバック関数を受け取る側の引数は(key: string, value: string, code: string)
  • 実際に渡しているコールバック関数の引数は(key: string, value: string)

となります。

このコードは問題ないです。

コールバック関数を受け取る側
const doSomethingWithKeyAndValueAndCode = (
  callbackfn: (key: string, value: string, code: string) => string
): Array<string> => {

  const data = [
    { key: "red", value: "", code: "#ff0000" },
    { key: "blue", value: "", code: "#0000ff" },
    { key: "gray", value: "", code: "#808080" },
  ];
  return data.map((item) => {
    return callbackfn(item.key, item.value, item.code); // ここでコールバック関数を実行
    // ↑余分な引数 item.code は捨てられる
  });
};
コールバック関数を渡す側
const myFn = (key: string, value: string): string => {
  return key + " " + value;
};

console.log(doSomethingWithKeyAndValueAndCode(myFn));
実行結果
["red 赤", "blue 青", "gray 灰"]

コールバック関数を受け取る側のコールバック関数の型を見ると、引数を3つ指定しなければいけないように見えますがそうではありません。
コールバック関数の引数は3つ(key: string, value: string, code: string)渡されるが、どの引数まで使うのかはコールバック関数を渡す側で決められる、ということになります。

この場合、余分な引数は捨てられます。

他の具体例で考えてみると、map関数のコールバック関数は第1引数にelement、第2引数にindex、第3引数にarrayが渡されるが、map関数のコールバック関数を指定する際は第1引数のelementだけを使うこともできる、というのがわかりやすいかと思います。

(第2引数だけ使いたい場合はmyArray.map((_, index) => index;のように、使わない引数をアンダースコア等にするのが慣例かと思います。)

コールバック関数を受け取るほうが、引数の数が少ない場合❌

以下のコードは、

コールバック関数を受け取る側の型は(key: string, value: string) => string
実際に渡しているコールバック関数の型は(key: string, value: string, code: string) => string

となっています。

引数の数に注目すると

  • コールバック関数を受け取る側の引数は(key: string, value: string)
  • 実際に渡しているコールバック関数の引数は(key: string, value: string, code: string)

となります。

このコードの場合、doSomethingWithKeyAndValue関数の実行部分でコンパイルエラーとなります。

コールバック関数を受け取る側
const doSomethingWithKeyAndValue = (
  callbackfn: (key: string, value: string) => string
): Array<string> => {

  const data = [
    { key: "red", value: "" },
    { key: "blue", value: "" },
    { key: "gray", value: "" },
  ];
  return data.map((item) => {
    return callbackfn(item.key, item.value); // ここでコールバック関数を実行
  });
};
コールバック関数を渡す側
const myFn = (key: string, value: string, code: string): string => {
  return key + " " + value + " " + code;
};

console.log(doSomethingWithKeyAndValue(myFn)); // myFnでコンパイルエラー

型 '(key: string, value: string, code: string) => string' の引数を型 '(key: string, value: string) => string' のパラメーターに割り当てることはできません。
Target signature provides too few arguments. Expected 3 or more, but got 2.

実行結果
["red 赤 undefined", "blue 青 undefined", "gray 灰 undefined"]

コールバック関数の引数は2つ(key: string, value: string)渡されるが、
実際に渡しているコールバック関数myFnは3つの引数の指定を必要としているため、
コールバック関数を受け入れることができずコンパイルエラーとなります。

(実行時、myFnの引数codeにはundefinedが渡され、文字列との連結時にundefinedも文字列に変換されるため、実行自体はできますが)

また、渡しているコールバック関数myFnの第3引数をオプショナルにするとコンパイルエラーは解消されます。

コールバック関数を渡す側
const myFn = (key: string, value: string, code?: string): string => {
  if (code) {
    return key + " " + value + " " + code;
  }
  return key + " " + value;
};

console.log(doSomethingWithKeyAndValue(myFn));
実行結果
["red 赤", "blue 青", "gray 灰"]

まとめ

コールバック関数を受け取る側について、コールバック関数の引数の数は多いほうが良い(多すぎるのも使いづらくなるが)

おわりに

ちなみに、冒頭の「サンプルコードの紹介」で紹介したコードについて、使い勝手が良くなるように書いたコードは以下のようになるかと思います。
ジェネリクスを使って、コールバック関数の返り値を任意に(返り値の型を広く)しています。
また、コールバック関数の返り値の型によって、doSomethingWithKeyAndValue関数の返り値の型も調整してくれます。

コールバック関数を受け取る側
const doSomethingWithKeyAndValue = <T>(
  callbackfn: (key: string, value: string) => T
): Array<T> => {
  const data = [
    { key: "red", value: "" },
    { key: "blue", value: "" },
    { key: "gray", value: "" },
  ];
  return data.map((item) => {
    return callbackfn(item.key, item.value); // ここでコールバック関数を実行
  });
};
コールバック関数を渡す側
const myFn = (key: string, value: string): string => {
  return key + " " + value;
};

console.log(doSomethingWithKeyAndValue(myFn));
実行結果
["red 赤", "blue 青", "gray 灰"]

参考文献

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?