TypeScriptの関数はどのように役立つのか(前編)TypeScriptのTypeあれこれシリーズ(3)

altJS、すなわち、JavaScriptの代わりとなる言語の筆頭である「TypeScript」。TypeScriptという言語名が示す通り、JavaScriptに「Type」、つまり、型の概念を持ち込んだものです。本連載では、このTypeScriptの型に関して、さまざまな方向から紹介していきます。前回はJavaScriptにはない、TypeScript独自の型を紹介しました。今回は関数で型を扱う場合を紹介していきます。基本となる引数や戻り値の型、さらには関数そのものの型を紹介します。

» 2021年11月26日 05時00分 公開

この記事は会員限定です。会員登録(無料)すると全てご覧いただけます。

「TypeScriptのTypeあれこれシリーズ」のインデックス

連載:TypeScriptのTypeあれこれシリーズ

関数の型はどう役立つのか

TypeScriptで関数を作る際の基本は、JavaScriptと同じです。ただし、型指定が追加されていきます。型指定があることで、どのように便利になるのか、そこから話を始めていきましょう。

JavaScriptの関数の問題点

 JavaScriptで関数を定義する場合、リスト1のようなコードを記述します。

function divide(num1, num2) {
	return num1 / num2;
}
リスト1

 この関数「divide()」は引数を2個受け取り、引数の割り算を実行した結果を返します。この関数を定義した際、処理内容から、その引数には数値が渡されることを前提にしています。ところが、JavaScriptでは、リスト2のコードのように、引数として文字列を渡しても、問題なく実行できてしまいます。

const ans = divide("こんにちは", "さようなら");
リスト2

 もちろん、関数の実行結果であるansは「NaN」(Not a Number)となり、さまざまな問題の原因になります。

関数の引数の型を指定できるTypeScript

 TypeScriptならば、コーディング段階でエラーにすることができます。それが、引数の型指定です。先ほどのdivide()関数の引数にデータ型を定義すると、リスト3のようになります。

function divide(num1: number, num2: number) {
	return num1 / num2;
}
リスト3

 このように定義した関数の引数に対して文字列を渡そうとすると、図1のようなエラーになります(以下、「Visual Studio Code」を利用)。

図1 数値型の引数に文字列を渡そうとしてエラーになった画面

関数の戻り値にも型を指定できる

 同様に、戻り値に対しても型指定ができます。例えば、先ほどのdivide()関数のように、戻り値として数値型を想定している関数の場合は、リスト4のように記述します。

function divide(num1: number, num2: number): number {
	return num1 / num2;
}
リスト4

 このように戻り値のデータ型「number」を定義した関数内で、違うデータ型の値を返すコードを記述すると、図2のようなエラーになります。

図2 数値型が戻り値の関数で文字列を返そうとしてエラーになった画面

 さらにreturn文を記述し忘れた場合も、図3のようなエラーになります。

図3 数値型が戻り値の関数でreturn文を記述せずにエラーになった画面

戻り値なしを表すvoid型

 戻り値のない関数を定義したい場合は、戻り値の型として「void」と記述します。例えば、次のような関数です。

function divide(num1: number, num2: number): void {
	const ans = num1 / num2;
	console.log(ans);
}
リスト5

 この場合、リスト4とは逆に、関数内にreturn文を記述すると、図4のようなエラーになります。

図4 戻り値がvoidの関数でreturn文を記述してエラーになった画面

 ここまでの内容を踏まえて、データ型を記述した関数シグネチャを構文としてまとめておきます。

[構文]関数シグネチャ
function 関数名(引数: データ型, ……): 戻り値のデータ型

特殊な戻り値の型を表すnever

 戻り値のデータ型には特殊なものがあります。「never」です。neverというデータ型は、「発生することがない値」を表し、戻り値がneverの関数は、そもそも制御が元に戻ってこない、という意味になります。例えば、次のような関数です。

function throwError(): never {
	throw new Error();
}
リスト6

 この関数は、関数内でエラーを発生させているため、そもそも呼び出し元に制御が戻りません。一方、さきほど説明したvoid型は、単に戻す値がないだけで、制御は呼び出し元に戻ります。

 リスト6の例だけでは、なかなかneverの使い所が分かりにくいかもしれません。実はneverとtypeof演算子を利用することで、関数内の記述漏れを防ぐことができます。これについては、次回の後編で紹介します。

TypeScriptならではの関数の引数定義

 もう一度、関数の引数の話に戻り、もう少し内容を掘り下げていきます。

引数を省略したとき、どうなるか

 JavaScriptでは、引数を省略しても特にエラーにはなりません。例えば、リスト7のようなコードです。

function twoStr(str1, str2) {  // (1)
	return str1 + str2;
}
const str = twoStr("こんにちは");  // (2)
リスト7

 (1)の関数「twoStr()」には引数が2個定義されていますが、それを呼び出している(2)では引数が1個しか渡されていません。このコードを実行すると、strの値が「こんにちはundefined」という変な値になりますが、問題なく実行できます。

 一方、TypeScriptでは、同じコードを記述すると、図5のようなエラーになります。なお、図5では関数シグネチャに、引数と戻り値の型「string」を追記しています。

図5 引数が不足しているためにエラーになった画面

 このように、TypeScriptでは、関数を呼び出す際の引数について、個数もデータ型も、引数の定義通りに渡す必要があります。

 もし、引数を省略可能にしたい場合は、引数名に「?」を付けます。例えば、図5のtwoStr()関数の第2引数を省略可能にしたい場合は、リスト8のような引数定義になります。

function twoStr(str1: string, str2?: string): string {
	return str1 + str2;
}
リスト8

 このような定義としておくと、図5のように第2引数を省略してもエラーにはなりません。

引数にはデフォルト値を持たせることができる

 もっとも、第2引数はデータがない状態ですから、strの値は相変わらず「こんにちはundefined」のままです。これを避けるために、ECMAScript 2015(ES2015)で導入された引数のデフォルト値を、TypeScriptで利用できます。例えば、twoStr()関数の第2引数にデフォルト値を設定すると、リスト9のようなコードになります。

function twoStr(str1: string, str2: string = "さようなら"): string {
	return str1 + str2;
}
リスト9

 ここで注意しなければならないのは、デフォルト値を設定している引数には、「?」の記述が不要だということです。記述すると、図6のようなエラーになります。

図6 「?」とデフォルト値の両方を記述してエラーになった画面

 なお、引数のデフォルト値を設定した場合、型推論が働くので、リスト10のように引数のデータ型指定を省略しても問題なく動作します。

function twoStr(str1: string, str2 = "さようなら"): string {
	return str1 + str2;
}
リスト10

可変長引数はなぜ必要なのか

 引数のデフォルト値と同じく、ES2015で関数に導入された仕組みとして、可変長引数があります。TypeScriptでは引数の個数とデータ型を定義した通りに使わなくてはならず、利用する段階で自由に変更できません。そのため、想定外の引数を受け取ることがなく、安心してコーディングできます。その一方で、作成段階で引数の個数を決めることができない関数の扱いに困ります。

 そのような場合に、可変長引数を利用します。例えば、リスト11のコードです。

function calcTotal(...nums: number[]): number {
	let total = 0;
	for(const num of nums) {
		total += num;
	}
	return total;
}
リスト11

 引数であるnumsの前に記述した「...」(ドット3個)が可変長引数を表します。引数が可変長であるため、calcTotal()関数を利用する際、引数を何個でも渡すことができます。例えば、リスト12の2行のコードを、どちらも問題なく実行できます。

const ans1 = calcTotal(1, 2, 3);
const ans2 = calcTotal(1, 2, 3, 4, 5);
リスト12

 ここで注目すべきなのは、引数のデータ型です。可変長引数は関数内で配列として扱われます。そのため、データ型は、「number[]」のように配列として記述します。文字列の可変長引数ならば、「string[]」とします。

 可変長引数の実態が配列であるため、前回解説したように、引数のデータ型が混在した状況は避けた方がよいでしょう。その意味でも、number[]やstring[]のように、可変長引数に同一のデータ型を指定できる仕組みが役立ちます。

 なお、引数が配列の形をとっているためループ処理に向いています。それを踏まえて、calcTotal()関数は、リスト11のようにループ処理させながら、合計値を計算するコードにしました。

関数そのもののデータ型はなぜ必要なのか

 リスト11では引数をループさせながら合計値の計算処理を実行しています。ループには「for-of構文」を利用しました。一方、配列には便利なループ構文があります。これを紹介しながら、関数そのもののデータ型へと話を発展させていきましょう。

コールバック関数シグネチャのデータ型記述

 配列オブジェクトには、わざわざfor-ofでループさせなくても、「forEach()メソッド」があり、自動でループできます。これを利用すると、リスト11をリスト13のように書き換えられます。

function calcTotal(...nums: number[]): number {
	let total = 0;
	nums.forEach(
		function(value: number, index: number, array: number[]): void {
			total += value;
		}
	);
	return total;
}
リスト13

 配列オブジェクトのforEach()メソッドは引数として、ループ1回ごとに行う処理が記述された関数を受け取ります。このように、引数として受け取る関数のことを、「コールバック関数」と呼びます。リスト13のコードでは、その関数を「無名関数」として記述しています。

 無名関数とは、名称の通り、関数名を記述しない関数です。従来の関数は、functionキーワードの次にcalcTotalのような関数名がありますが、無名関数にはこれがありません。functionのすぐ後に引数定義が続くのが構文上の特徴です。無名関数は名前がないため、再度呼び出すことができず、使い捨ての関数になります。

 リスト13ではコールバック関数の引数の定義は、第1引数が今の繰り返し処理で対象としている配列の要素value、第2引数が同じくインデックスindex、第3引数が配列そのもののarrayです。これを、JavaScriptでは、単にリスト14のように記述します。

function(value, index, array) {
リスト14

 TypeScriptでは、コールバック関数に記述する無名関数であっても、それぞれの引数の型と戻り値の型を記述できます。なお、forEach()のコールバック関数のようにあらかじめ引数が定義されているものでは、元の配列から自動的に型が決まっています。リスト13では、第1引数と第2引数がnumber型、第3引数がnumberの配列と決まっています。これは、元の配列がnumberの配列だからです。

 もしもstringの配列であれば、関数シグネチャは、リスト15のようになります。

function(value: string, index: number, array: string[]): void {
リスト15

 インデックスは数値と決まっているので第2引数は依然number型ですが、第1引数と第3引数は、元の配列に合わせてstring型とstringの配列になっています。

 このように、コールバック関数のデータ型を意識することで、関数内の記述にもバグが紛れ込みにくくなるのです。

アロー関数でも戻り値のデータ型を記述できる

 先ほどの例では、forEach()のコールバック関数として、無名関数を利用しました。この部分をES2015で導入された「=>」を用いる「アロー関数」に置き換えることができます。

 アロー関数は無名関数を簡略化できる記述です。functionキーワードがない代わりに、引数の「( )」と処理ブロックの「{ }」の間に「=>」を記述することが特徴です。アロー関数を使った場合でも無名関数と同様に、引数と戻り値のデータ型を、リスト16のように記述できます。

nums.forEach(
	(value: number, index: number, array: number[]): void => {
		total += value;
	}
);
リスト16

関数式でのデータ型

 コールバック関数の記述方法として無名関数やアロー関数ではなく、それらを1つの変数に格納した関数式を利用することもできます。その場合は、リスト17のコードになります。

function calcTotal(...nums: number[]): number {
	let total = 0;
	const loopFunc = function(value: number, index: number, array: number[]): void {
		total += value;
	}
	nums.forEach(loopFunc);
	return total;
}
リスト17

 このコードでは、forEach()に引数として渡すループ処理関数を、loopFuncという変数に格納しています。その後forEach()の引数として渡しています。変数に格納する際も、これまでの関数定義と同様に、引数と戻り値の型を記述しています。

 この関数式をアロー関数式でも記述でき、リスト18のコードになります。

const loopFunc = (value: number, index: number, array: number[]): void => {
	total += value;
}
リスト18

関数のデータ型

 ここで考えなければならないことがあります。リスト17や18の変数loopFuncのデータ型は、一体どのようなものなのかということです。

 JavaScriptでは、関数そのものをデータの一種として捉え、先ほどの関数式のように変数に格納したり、コールバック関数のように引数として定義したりできます。この仕組みは、TypeScriptでも同様です。一方で、TypeScriptでは、変数でも引数でもデータ型が存在します。

 ということは、この関数を格納する変数や関数そのものが引数であるコールバック関数にも、当然データ型があり、型としての記述が可能です。

 実は、リスト17や18のloopFunc変数には、関数を格納する時点で型推論が働いており、変数そのものにデータ型を記述する必要がありません。これをあえて記述すると、リスト19のようになります。

const loopFunc: (value: number, index: number, array: number[]) => void
リスト19

 関数のデータ型記述を構文としてまとめました。

[構文]関数そのもののデータ型
(引数名: 引数のデータ型, ……) => 戻り値の型

 注意しなければならないのは、この引数名はあくまで仮であり、必ずしも、この後に定義する関数の引数名と一致する必要はない、ということです。一致しなければならないのは、あくまでデータ型です。このため、リスト20のような記述であっても、問題なく動作します。

const loopFunc: (v: number, i: number, a: number[]) => void = function(value: number, index: number, array: number[]): void {
	total += value;
}
リスト20

コールバック関数定義でのデータ型記述

 とはいえ、本来は型推論が働くため、関数式を格納する変数にデータ型をわざわざ記述することはあまりありません。関数そのもののデータ型記述が活躍するのは、コールバック関数を定義、すなわち、関数の引数を関数とする場合です。

 例えば、次のような関数calc2Rand()を想定します。calc2Rand()関数内で0〜10の乱数を2個発生させ、それぞれrand1、rand2とします。これらはnumber型です。そして、その2個の数値を何か演算処理させ、その結果をcalc2Rand()の戻り値にします。そのため、calc2Rand()の戻り値のデータ型は、numberです。

 ここでのポイントは、演算処理内容を関数calc2Rand()内で決めるのではなく、引数として受け取るようにすることです。そのため、calc2Rand()の引数は、コールバック関数になります。そして、このコールバック関数である引数名をfuncとします。すると、calc2Rand()関数は、リスト21のようなコードになります。

function calc2Rand(func: (rand1: number, rand2: number) => number): number {
	const rand1 = Math.round(Math.random() * 10);
	const rand2 = Math.round(Math.random() * 10);
	return func(rand1, rand2);
}
リスト21

 ここで注目するのは、引数funcのデータ型です。この引数は数値(number)や文字列(string)ではなく、関数ですので、関数としてのデータ型を記述します。それが、リスト21の引数に記述されたリスト22の部分です。

(rand1: number, rand2: number) => number
リスト22

 この型指定があるおかげで、例えば、文字列を返すような関数をコールバック関数として記述すると、図7のようなエラーになります。

図7 シグネチャが違う関数をコールバックとして記述してエラーになった画面

 そして、コールバック関数funcの戻り値がそのままcalc2Rand()の戻り値となるため、仮に文字列を返す関数が図7のようにエラーとならないならば、calc2Rand()の戻り値も文字列になってしまいます。実際、calc2Rand()がリスト23のようなシグネチャならば、図7のエラーは表示されません。

function calc2Rand(func): number
リスト23

 このcalc2Rand()の戻り値を数値だと想定して、他の演算に利用しようとしているなら、実行して初めて気付くバグになります。ところが、TypeScriptでは、コールバック関数の型指定ができるため、図7のように実行エラーではなく、コーディングミスとして修正できるようになります。型指定があるとないとでは、コーディング効率が変わってくるのが分かるでしょう。

まとめ

 TypeScriptのTypeに注目し、あれこれ紹介する本連載の第3回はいかがでしたか? 今回は、関数の引数や戻り値に型指定を行う方法を紹介しました。さらには、関数そのものの型を紹介しました。

 次回は、後編として、関数の型にまつわるあれこれの紹介を続けます。

筆者紹介

WINGSプロジェクト

有限会社 WINGSプロジェクトが運営する、テクニカル執筆コミュニティー(代表山田祥寛)。主にWeb開発分野の書籍/記事執筆、翻訳、講演等を幅広く手掛ける。2021年10月時点での登録メンバーは55人で、現在も執筆メンバーを募集中。興味のある方は、どしどし応募頂きたい。著書、記事多数。

サーバーサイド技術の学び舎 - WINGS(https://wings.msn.to/
RSS(https://wings.msn.to/contents/rss.php
Twitter: @yyamada(https://twitter.com/yyamada
Facebook(https://www.facebook.com/WINGSProject


Copyright © ITmedia, Inc. All Rights Reserved.

スポンサーからのお知らせPR

注目のテーマ

Microsoft & Windows最前線2025
AI for エンジニアリング
ローコード/ノーコード セントラル by @IT - ITエンジニアがビジネスの中心で活躍する組織へ
Cloud Native Central by @IT - スケーラブルな能力を組織に
システム開発ノウハウ 【発注ナビ】PR
あなたにおすすめの記事PR

RSSについて

アイティメディアIDについて

メールマガジン登録

@ITのメールマガジンは、 もちろん、すべて無料です。ぜひメールマガジンをご購読ください。