本文へスキップ
スキルアップカレッジ

関数に型をつける

レッスン4:関数に型をつける

このレッスンで学ぶこと

  • 関数の引数と戻り値に型注釈を付けられる
  • 省略可能な引数とデフォルト値を型で表現できる
  • 値を返さない関数の型を理解する
  • コールバック関数に型を付けられる

レッスン3 では、型推論とユニオン型を使って、複数の可能性を持つ値を安全に扱う方法を学びました。このレッスンでは、視点を値そのものから「関数」に移します。関数は、コードの中でもっとも頻繁に型の恩恵を受ける部分です。呼び出す側と呼び出される側が離れた場所にあることが多いため、「何を渡せば、何が返ってくるのか」を型で示しておく効果が大きくなります。

「JavaScript 入門」のレッスン4 では、関数を使うメリットとして「同じ処理の使い回し」「意図の明確化」「修正のしやすさ」の 3 つを確認しました。型注釈は、この中の「意図の明確化」をさらに一歩進める道具です。関数名だけでなく、引数と戻り値の型まで見れば、中身を読まなくても使い方がわかるようになります。

引数に型を付ける

「JavaScript 入門」で学んだとおり、関数の引数は呼び出すたびに違う値を渡せる仕組みです。TypeScript では、その引数それぞれに型注釈を付けられます。

function calculateTax(price: number, taxRate: number) {
  return price + price * taxRate;
}

console.log(calculateTax(1000, 0.10));

price: number と taxRate: number が引数への型注釈です。この関数を、文字列を渡して呼び出そうとすると、実行前に止められます。

calculateTax("1000", 0.10);
error TS2345: Argument of type 'string' is not assignable to parameter of type 'number'.

関数の引数が増えてくるほど、「どの順番で、どんな値を渡すべきか」を覚えておくのは難しくなります。型注釈があれば、その負担をコードの記述そのものに肩代わりさせられます。レッスン1 で見た「引数の順番の取り違え」の事故も、この型注釈で防げます。

function createUser(name: string, age: number) {
  return { name: name, age: age };
}

const user = createUser(30, "森田 彩花");
error TS2345: Argument of type 'number' is not assignable to parameter of type 'string'.

name に数値、age に文字列を渡そうとしているため、1 つ目の引数の時点でエラーになります。関数の引数に型を付けるだけで、レッスン1 で紹介した事故の多くが、実行する前の段階で発見できるようになります。

戻り値に型を付ける

引数だけでなく、関数が返す値にも型注釈を付けられます。書く位置は、引数リストの閉じ括弧の後ろにコロンを置く形です。

function calculateTax(price: number, taxRate: number): number {
  return price + price * taxRate;
}

): number { の部分が戻り値の型注釈です。「この関数は必ず number 型の値を返す」という宣言になります。戻り値の型を書いておくと、関数の中身が正しく number を返しているかどうかを、TypeScript がチェックしてくれます。

function calculateTax(price: number, taxRate: number): number {
  return `${price * taxRate}`; // 文字列を返しているのでエラー
}
error TS2322: Type 'string' is not assignable to type 'number'.

戻り値の型注釈がなくても、多くの場合は型推論が働き、戻り値の型は自動的に決まります。それでも戻り値の型を明示的に書いておくと、「この関数は何を返すはずか」という意図が、関数の定義を見ただけで伝わります。特に、複数人でコードを触るチーム開発や、後で読み返す自分自身のために、戻り値の型注釈は書いておく価値があります。

💡 ポイント 引数の型注釈はほぼ必須ですが、戻り値の型注釈は「推論に任せてもよい」場面と「明示したほうがよい」場面に分岐します。関数の中身が長い場合や、公開する関数(ほかのファイルから使われる関数)では、戻り値の型を明示しておくと安全です。関数の中身を後から書き換えたときに、意図しない型の値を返していないかをすぐに確認できる利点もあります。

アロー関数に型を付ける

「JavaScript 入門」で学んだアロー関数にも、同じように型注釈を付けられます。書く位置は function 宣言とほぼ同じで、引数の後ろに戻り値の型を書きます。

const calculateTax = (price: number, taxRate: number): number => {
  return price * taxRate;
};

const formatYen = (amount: number): string => `${amount}円`;

calculateTax は波括弧を使った通常の書き方、formatYen は 1 行で値を返す省略形です。省略形であっても、引数と戻り値への型注釈の書き方は変わりません。formatYen に文字列を渡そうとすると、function 宣言のときと同じようにコンパイルの段階で止められます。

formatYen("2000"); // エラー
error TS2345: Argument of type 'string' is not assignable to parameter of type 'number'.

function 宣言とアロー関数のどちらを使う場面が多いかは、「JavaScript 入門」で扱った使い分けの方針がそのまま引き継がれます。トップレベルの独立した関数には function 宣言、配列の処理やイベント処理などその場で短く書く関数にはアロー関数、という基準はそのまま活かせます。型注釈の書き方だけが新しく加わったと考えれば、迷うことはありません。

省略可能な引数——渡さなくてもよい引数を作る

引数の中には、「渡しても渡さなくてもよい」ものがあります。TypeScript では、引数名の後ろに ? を付けることで、省略可能な引数を表せます。

function greet(name: string, honorific?: string) {
  if (honorific !== undefined) {
    console.log(`${honorific} ${name}さん、こんにちは`);
  } else {
    console.log(`${name}さん、こんにちは`);
  }
}

greet("森田");
greet("森田", "講師");

honorific?: string の ? が、省略可能であることの印です。greet("森田") のように 2 つ目の引数を渡さなくても、コンパイルエラーにはなりません。省略された場合、honorific の値は undefined になるため、レッスン3 で学んだ型の絞り込みと同じ考え方で、値があるかどうかを確認してから使います。

省略可能な引数には、1 つ制約があります。

function greet(honorific?: string, name: string) { // エラー
  // ...
}
error TS1016: A required parameter cannot follow an optional parameter.

「省略可能な引数の後ろに、必須の引数を置くことはできません」という指摘です。呼び出し側が引数を 2 つだけ渡したとき、「1 つ目を省略したのか、2 つ目を省略したのか」が区別できなくなるためです。省略可能な引数は、必ず引数リストの末尾側にまとめて置きます。

デフォルト値——省略時の初期値を決める

省略可能な引数と似た働きをするものに、デフォルト値があります。引数に = で初期値を指定しておくと、呼び出し時に値が渡されなかった場合、その初期値が使われます。

function greet(name: string, honorific: string = "様") {
  console.log(`${honorific} ${name}さん、こんにちは`);
}

greet("森田");        // "様 森田さん、こんにちは"
greet("森田", "先生"); // "先生 森田さん、こんにちは"

honorific: string = "様" は、「型は string で、値が渡されなければ "様" を使う」という意味です。デフォルト値がある引数は、型注釈を省略しても、初期値から型が自動的に推論されます。

? を使った省略可能な引数と、デフォルト値付きの引数は似ていますが、関数の内部での扱いが異なります。? の場合は関数の中で undefined かどうかを毎回確認する必要がありますが、デフォルト値がある場合は、関数の中では常に値が入っていることが保証されます。「省略されたときに undefined のまま扱いたいか、決まった初期値を使いたいか」で使い分けます。

判断に迷ったときの目安は、「省略されたときに何もしない・特別扱いする」なら ?、「省略されたときも普段どおりの動作をさせたい」ならデフォルト値、という考え方です。消費税率のように、多くの場合は同じ値を使い、まれに違う値を試したいという場面では、デフォルト値のほうが扱いやすくなります。

📝 補足 引数の型注釈には、このレッスンで扱った以外にも細かい書き方がいくつかあります。例えば、引数の数が決まっていない関数を作る書き方もありますが、入門段階では扱う機会が少ないため、本コースでは割愛します。まずはここで扱った基本形を確実に使えるようにしておけば十分です。

値を返さない関数の型——void

console.log を呼び出すだけで、return を書かない関数もよくあります。このような関数の戻り値の型には、void を使います。

function logUserInfo(name: string, age: number): void {
  console.log(`${name}さん、${age}歳です`);
}

void は「何も意味のある値を返さない」ことを表す型です。logUserInfo の戻り値を変数に受け取って使おうとすると、TypeScript は「意味のある値ではない」ことを教えてくれます。

const result = logUserInfo("森田", 30);
console.log(result.toUpperCase()); // result は使い道のない値のため、この先の操作はできない

戻り値の型注釈は、多くの場合、return を書いていなければ TypeScript が void と自動的に推論してくれます。それでも void と明示しておくと、「この関数は値を返す設計ではない」という意図を、読み手にはっきり伝えられます。

void と undefined は、どちらも「値がない」ことに関わる型ですが、役割が異なります。undefined は「値そのものが未設定であること」を表す値の型で、レッスン3 のユニオン型の中でも使いました。void は「関数が返すことを期待されていない」という、関数の設計そのものを表す型です。値として undefined を返す関数と、そもそも値を返す設計になっていない関数を区別するために、この 2 つは使い分けられています。

コールバック関数の型——関数を引数として受け取る

「JavaScript 入門」で学んだとおり、forEach や addEventListener のように、関数を引数として受け取る関数(コールバック関数を受け取る関数)は数多くあります。TypeScript では、関数の型そのものにも注釈を付けられます。

function processScore(score: number, callback: (result: string) => void) {
  const result = score >= 80 ? "合格" : "不合格";
  callback(result);
}

processScore(85, (result) => {
  console.log(`判定:${result}`);
});

callback: (result: string) => void の部分が、コールバック関数の型注釈です。「string 型の引数を 1 つ受け取り、何も返さない関数」という形を表しています。processScore を呼び出す側は、この形に合った関数を渡さなければなりません。

processScore(85, (result: number) => { // エラー:result の型が合わない
  console.log(result);
});
error TS2345: Argument of type '(result: number) => void' is not assignable to parameter of type '(result: string) => void'.

コールバック関数の型を明示しておくことで、「このコールバックには、こういう形の関数を渡してください」という約束を、関数の定義自体が語ってくれるようになります。

配列の要素を 1 件ずつ処理する場面でも、コールバック関数の型は同じように働きます。

function renderNames(names: string[], onEach: (name: string, index: number) => void) {
  names.forEach((name, index) => {
    onEach(name, index);
  });
}

renderNames(["森田", "佐藤", "田中"], (name, index) => {
  console.log(`${index + 1}番目:${name}`);
});

onEach の型 (name: string, index: number) => void は、forEach に渡すコールバックとほぼ同じ形をしています。TypeScript の標準ライブラリに含まれる forEach や map の型定義も、内部的にはこれと同じ考え方で作られています。自分で作る関数にも同じ形の型注釈を使えると知っておくと、既存のメソッドの型定義を読むときの理解も深まります。

🔰 初学者の方へ コールバック関数の型注釈は、最初は読みづらく感じるかもしれません。(引数名: 型) => 戻り値の型 という並びを、「引数の形と戻り値の形をひとまとめにした、小さな関数専用の型」と捉えると理解しやすくなります。

実習:既刊の関数を型付きで書き直す

「JavaScript 入門」で書いた自己紹介用の関数を、型注釈付きで書き直してみましょう。

function introduce(name: string, age: number, hobby?: string): string {
  const hobbyText = hobby !== undefined ? `趣味は${hobby}です。` : "";
  return `私は${name}、${age}歳です。${hobbyText}`;
}

function calculateTax(price: number, taxRate: number = 0.10): number {
  return price * taxRate;
}

function printReceipt(itemPrice: number, formatter: (amount: number) => string): void {
  const tax = calculateTax(itemPrice);
  console.log(`商品価格:${formatter(itemPrice)}`);
  console.log(`消費税:${formatter(tax)}`);
  console.log(`合計:${formatter(itemPrice + tax)}`);
}

console.log(introduce("森田 彩花", 30, "登山"));
console.log(introduce("佐藤 健", 25));

printReceipt(2000, (amount) => `${amount}円`);

introduce では省略可能な引数、calculateTax ではデフォルト値、printReceipt ではコールバック関数の型注釈を、それぞれ使っています。型注釈を付けたことで、これらの関数を別の場所から呼び出すときに「何を渡せばよいか」を、関数の中身を読まなくても判断できるようになりました。

「JavaScript 入門」で最初にこれらの関数を書いたときと見比べると、コードの行数自体はそれほど大きくは増えていないことに気づくはずです。型注釈は、コードをいたずらに複雑にするための追加要素ではなく、もともとコードの中に暗黙のうちにあった前提(この引数には数値しか渡さないつもりだった、この関数は値を返さないつもりだった)を、目に見える形にしただけのものです。

まとめ

このレッスンでは、以下のことを学びました。

  • 関数の引数には 引数名: 型、戻り値には ): 型 { の形で型注釈を付ける
  • 省略可能な引数は 引数名?: 型、デフォルト値付きの引数は 引数名: 型 = 初期値 で書く
  • 省略可能な引数は、引数リストの末尾側にまとめて置く
  • 値を返さない関数の戻り値には void を使う
  • コールバック関数には (引数名: 型) => 戻り値の型 の形で型を付けられる

このレッスンで扱った関数への型注釈は、書き方こそシンプルですが、実務のコードでもっとも頻繁に、繰り返し登場する形です。関数を書くたびに「引数は何を受け取るか」「何を返すか」を意識する習慣が付くと、レッスン1 で確認した 4 つの事故のうち、かなりの部分を実行前に防げるようになります。

次のレッスンでは、関数だけでなく、オブジェクトの形そのものに名前を付ける「インターフェース」と「型エイリアス」を学びます。同じ形を何度も書く手間を減らし、構造的型付けという TypeScript らしい考え方をしっかり身につけましょう。


確認クイズ

このレッスンの理解度をチェックしましょう。