TypeScript | 関数・クラス・ジェネリクス:関数設計の深化 – 関数宣言と関数式の違い

TypeScript
スポンサーリンク

まず「関数宣言」と「関数式」をざっくり区別する

最初に形だけ見て違いを押さえましょう。

関数宣言(function declaration)

function add(a: number, b: number): number {
  return a + b;
}
TypeScript

関数式(function expression)

const add = function (a: number, b: number): number {
  return a + b;
};
TypeScript

どちらも「add という関数」を作っています。
でも、振る舞い・書き心地・型の付け方に、じわっと効いてくる違いがあります。

ここからは、その違いを「初心者が実務で困らない」レベルまで丁寧に分解していきます。


一番大きな違い:ホイスティング(いつから使えるか)

関数宣言は「ファイルのどこからでも呼べる」

関数宣言は、定義より前で呼び出しても動きます。

greet("Taro"); // ここで呼んでもOK

function greet(name: string) {
  console.log("こんにちは、" + name);
}
TypeScript

JavaScript の仕様として、「関数宣言はファイル読み込み時に先に登録される」ため、
コード上では後ろに書いてあっても、前から呼べます。
これをホイスティング(持ち上げ)と呼びます。

TypeScript でもこの挙動は同じです。

「ファイルの上の方で関数を使いたい」「下の方にまとめて定義したい」
というとき、関数宣言は相性がいいです。

関数式は「定義されたあとからしか使えない」

関数式は、変数に代入して使います。

greet("Taro"); // エラー:まだ greet が定義されていない

const greet = function (name: string) {
  console.log("こんにちは、" + name);
};
TypeScript

const greet = ... の行より前では、greet はまだ存在しません。
これは「変数のホイスティング」と関係していて、
constlet は「宣言はされるけど、初期化前には使えない」からです。

つまり、関数式は「その行より後ろ」でしか使えません。

この違いから言えることは、

関数宣言は「ファイル全体で使う“トップレベルの機能”」
関数式は「あるスコープの中で使う“値としての関数”」

という役割分担がしっくりきます。


型の付き方の違い:宣言はシグネチャ、式は「変数の型」として

関数宣言は「その場で型を書く」

関数宣言では、引数と戻り値に直接型を書きます。

function add(a: number, b: number): number {
  return a + b;
}
TypeScript

この書き方はシンプルで読みやすく、
「この関数はこういうものです」と一発で分かります。

また、関数宣言は「関数オーバーロード」とも相性がいいです(後で触れます)。

関数式は「変数の型」として関数型を書く

関数式では、2通りの型の付け方があります。

1つ目は、関数本体に直接書く方法。

const add = function (a: number, b: number): number {
  return a + b;
};
TypeScript

2つ目は、「変数の型」として関数型を書く方法です。

const add: (a: number, b: number) => number = (a, b) => {
  return a + b;
};
TypeScript

後者の書き方は少し長いですが、
「この変数は“こういう形の関数”を入れるためのものだ」と宣言しているので、
関数を差し替えたり、別のファイルから渡したりするときに強いです。

TypeScript らしい設計をしていくと、
「関数を引数として受け取る」「関数を返す」場面が増えます。
そのときは、関数式+関数型((a: number) => string など)の組み合わせが主役になります。


関数宣言が得意なこと:オーバーロードと「APIの顔」を作る

関数オーバーロードは「宣言」で書くのが基本

TypeScript には「同じ関数名で、引数のパターンだけ変える」オーバーロードがあります。

function toArray(value: string): string[];
function toArray(value: number): number[];
function toArray(value: string | number): (string | number)[] {
  return [value];
}
TypeScript

上の2行が「オーバーロードシグネチャ」、
最後の1行が「実装」です。

このように、関数宣言は

「外から見える顔(オーバーロードのパターン)」
「中の実装」

をきれいに分けて書けます。

関数式でもオーバーロードは書けますが、
基本的には関数宣言の方が自然で読みやすいです。

「ライブラリのAPIのように、“この関数はこういう使い方ができます”と宣言したい」
というとき、関数宣言はとても向いています。


関数式が得意なこと:コールバック・高階関数・thisのない世界

コールバックとして渡すときは、関数式が自然

例えば、配列の map に渡す関数。

const numbers = [1, 2, 3];

const doubled = numbers.map(function (n) {
  return n * 2;
});
TypeScript

あるいはアロー関数で書くことが多いです。

const doubled = numbers.map((n) => n * 2);
TypeScript

ここでは「一度きりの小さな関数」をその場で定義して渡しています。
こういう「その場限りの関数」は、関数宣言より関数式の方がしっくりきます。

TypeScript 的にも、
map の引数は (value: number, index: number) => U です」と型が決まっているので、
関数式に対して型推論が効きやすいです。

アロー関数(関数式の一種)は this を固定してくれる

クラスやオブジェクトのメソッドで this を扱うとき、
アロー関数(=関数式の一種)はとても便利です。

class Counter {
  count = 0;

  // 関数宣言スタイルのメソッド
  increment() {
    this.count++;
  }

  // 関数式(アロー関数)をプロパティに持つ
  incrementLater = () => {
    setTimeout(() => {
      this.count++; // this がクラスインスタンスを指す
    }, 1000);
  };
}
TypeScript

アロー関数は this を外側から「捕まえて」固定してくれるので、
コールバックの中で this が変わってしまう問題を避けられます。

「this に悩まされたくない」「クラスのメソッドをコールバックとして渡したい」
というとき、関数式(特にアロー関数)は強力な選択肢になります。


設計の視点:どっちをいつ使うか

「名前付きのトップレベルAPI」は関数宣言が向いている

例えば、モジュールの外に公開する関数や、
「このファイルの主役」となる処理は、関数宣言で書くと読みやすいです。

export function parseUser(json: string): User {
  // ...
}
TypeScript

ファイルを開いたときに、

「あ、このファイルは parseUser という関数を提供しているんだな」

と一目で分かります。

また、オーバーロードを使いたいときも関数宣言が基本です。

「値として扱う関数」「差し替え可能な関数」は関数式が向いている

一方で、

設定として関数を渡したい
テストでモック関数に差し替えたい
クラスのプロパティとして関数を持ちたい

といった場面では、「関数を値として扱う」ことが多くなります。

type Logger = (message: string) => void;

const consoleLogger: Logger = (message) => {
  console.log(message);
};

function runTask(task: () => void, logger: Logger) {
  logger("タスク開始");
  task();
  logger("タスク終了");
}
TypeScript

ここでは、Logger という「関数の型」を定義し、
それを満たす関数式を変数に入れています。

このように、「関数を変数に入れて持ち運ぶ」「引数として渡す」設計は、
関数式+関数型の組み合わせが本領発揮するところです。


初心者向けの指針:「迷ったときの決め方」

まずはこう決めてしまっていい

基礎段階では、次のように割り切ってしまって構いません。

ファイルの外に公開する「メインの処理」
→ 関数宣言で書く

一度きりの小さな処理・コールバック・設定として渡す関数
→ 関数式(特にアロー関数)で書く

関数の型を別名として定義して使いたいとき
→ 関数式+(arg: T) => U 形式の関数型を使う

慣れてくると、「ここは this を固定したいからアロー関数にしよう」
「ここはオーバーロードしたいから関数宣言にしよう」
といった判断が自然にできるようになります。


まとめ:「関数宣言 vs 関数式」を自分の言葉で整理する

最後に、あなた自身の言葉でこう言い換えてみてください。

関数宣言は
「ファイル全体から呼ばれる“名前付きの機能”を定義するもの。
ホイスティングされるので、前からでも後ろからでも呼べる。
オーバーロードや“APIの顔”を作るのに向いている。」

関数式は
「関数を“値”として扱うための書き方。
変数に入れたり、引数として渡したり、クラスのプロパティにしたりできる。
アロー関数と組み合わせると this 問題も避けやすい。」

コードを書いていて、

「この関数は“どこからどう使われる存在”なんだろう?」

と一度立ち止まって考えてみてください。

その答えが、
関数宣言で書くか、関数式で書くか、
そしてどんな型を付けるか、
という設計の選択につながっていきます。

タイトルとURLをコピーしました