投稿日:2022年2月11日
呼び出しシグネチャ自体はそこまで難しい内容ではないのですが、ただ一つ、返り値がvoidの時については扱い方に気を付ける必要があります。この記事では呼び出しシグネチャの返り値がvoidを指定した場合の注意点、またその解決方法について詳しく解説しています。
TypeScriptでは関数を扱う際に、定義する型宣言とは別に呼び出しシグネチャ(Call Signatures)というものが存在します。
呼び出しシグネチャ自体はそこまで難しい内容ではないのですが、ただ一つ、返り値がvoidの時については、扱い方に気を付ける必要があります。
この記事では呼び出しシグネチャの返り値がvoidを指定した場合の注意点、またその解決方法について詳しく解説しています。
この記事のサンプルコードは以下の環境で確認しています。
この記事のサンプルコードは以下で公開しています。
まずは事前の知識として関数の型定義から、今回の本題である戻り値の型について簡単に確認しておきましょう。
まずは簡単な例になります。
// 値を数値型で指定し、数値をreturn
function func_1(a: string): number {
return 10;
}
// 戻り値をvoidで指定、returnを呼び出さない
function func_2(a: string): void {
}
// 戻り値をvoidで指定、returnでなにも返さない
function func_3(a: string): void {
return;
}
// 戻り値がnumberなのに何も返していない
function func_1_error_1(a: string): number {
}
// 戻り値がvoidなのに値を返す
function func_2_error_1(a: string): void {
return 10;
}
ここまではかなり基本的な仕様ですね。
他のプログラミング言語と全く大差ない仕様になります。
callbackの型を指定する場合などには、割り当てられる関数の型を定義する必要があります。
これを呼び出しシグネチャ(Call Signatures)と言います。
まずは簡単な例をみてみましょう。
// 呼び出しシグネチャ(オブジェクトタイプ)
type Func4 = {
(a: string): number
}
const func_4_variable : Func4 = function func_4(a) {
return 10;
}
// 呼び出しシグネチャ(アロータイプ)
type Func5 = (a: string) => number;
const func_5_variable: Func5 = (a) => {
return 10;
}
// 呼び出しシグネチャ、戻り値がvoid
type Func6 = (a: string) => void;
const func_6_variable: Func6 = function func_6(a) {
return;
}
呼び出しシグネチャにはFunc4の様にオブジェクトで定義する方式とFunc5の様にアロー式で定義する方式の2パターンあります。
見ての通り定義自体に難しい点はないと思います。
呼び出しシグネチャがどの様なパターンでエラーになるのかをみていきましょう。
まずは最も基本的なパターンです。
// 呼び出しシグネチャの戻り値がnumberなのに戻り値も違う
type Func4Error1 = {
(a: string): number
}
const func_4_variable_error_1: Func4Error1 = function func_4_error_1(a) {
}
const func_4_variable_error_2: Func4Error1 = function func_4_error_2(a) {
return "Hello World"
}
type Func7 = (a: string) => void;
const func_7_variable: Func7 = (a) => {
return 10;
}
変数に値や関数を割り当てる場合には割り当て可能性(assignability)を考慮する必要があります。
TypeScriptの場合、割り当てる関数の型が割り当てられる変数の型のサブタイプである必要があります。
関数A、関数Bが以下の全てを満たす場合、関数Aは関数Bのサブタイプであると言えます。
今回は3の条件が問題になります。
まず簡単に以下の様なパターンを考えましょう。
// 返り値がstringまたはnumberを要求
type Func8 = (a: string) => string | number;
const func_8_variable: Func8 = (a): number => {
return 10;
}
呼び出しシグネチャの戻り値としてのvoidは、まれに非常にわかりにくいバグを発生させます。
最後に、どの様な場合にバグになるのか簡単な例を載せておきます。
/**
* ただ与えられた関数を実行するだけのクラス
*/
class SampleExecuter{
executeFunc (first: () => void, second: () => void) {
first();
second();
}
}
/**
* モンスターが現れるまでのセリフを管理する関数
* @returns
*/
const priorConversation = (): Promise<void> => {
return new Promise<void>((resolve, reject) => {
console.log("おっさん「この辺りはモンスターが出るぞ、気をつけろ」");
setTimeout(() => {
console.log("おっさん「そこにいるのは…モンスターだ!」");
resolve();
},3000);
});
}
/**
* モンスターとバトルする処理
*/
const startBattle = (): Promise<void> => {
return new Promise<void>((resolve, reject) => {
console.log("モンスターが現れた!!!");
setTimeout(() => {
console.log("おっさんの攻撃!!!モンスターを倒した!!!");
},2000);
});
}
// 実行
const sampleExecuter = new SampleExecuter();
sampleExecuter.executeFunc(priorConversation, startBattle);
SampleExecuterクラスのexecuteFunc()は与えられた関数をただ順番に実行するだけの関数です。executeFunc()の引数であるfirst、second、はどちらも戻り値がvoidの呼び出しシグネチャが設定してあります。
priorConversation()とstartBattle()は、どちらもコンソールに文字を出力するだけの関数です。この二つの関数をSampleExecuterにのexecuteFunc()に渡すのですが、ここで問題があります。
executeFunc()の引数であるfirst、secondはどちらも同期関数の想定だったのですが、実際に渡されたpriorConversation()はPromiseを返す非同期関数なのです。
これをコンパイルして実行してみると以下の様な結果になります。
おっさん「この辺りはモンスターが出るぞ、気をつけろ」
モンスターが現れた!!!
おっさんの攻撃!!!モンスターを倒した!!!
おっさん「そこにいるのは…モンスターだ!」
/**
* ただ与えられた関数を実行するだけのクラス
*/
class SampleExecuter{
async executeFunc (first: () => void | Promise<void>, second: () => void | Promise<void>) {
await first();
await second();
}
}
// [...]
この実装の場合first、secondに渡されたのが同期関数でも非同期関数でも、ちゃんと順番通りに実装してくれます。
これはあくまで解決法の一つです。
単純にexecuteFunc()に非同期関数を渡すことを禁止したい場合などには、別の方法として、呼び出しシグネチャの戻り値の型をnullにするなどの対応も考えられるでしょう。また、非同期関数を渡すメソッドを別で作る等の対応も考えられます。
同じ様な状況に遭遇した時には、その都度最適な対処法を考えましょう。