まず「関数宣言」と「関数式」をざっくり区別する
最初に形だけ見て違いを押さえましょう。
関数宣言(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);
}
TypeScriptJavaScript の仕様として、「関数宣言はファイル読み込み時に先に登録される」ため、
コード上では後ろに書いてあっても、前から呼べます。
これをホイスティング(持ち上げ)と呼びます。
TypeScript でもこの挙動は同じです。
「ファイルの上の方で関数を使いたい」「下の方にまとめて定義したい」
というとき、関数宣言は相性がいいです。
関数式は「定義されたあとからしか使えない」
関数式は、変数に代入して使います。
greet("Taro"); // エラー:まだ greet が定義されていない
const greet = function (name: string) {
console.log("こんにちは、" + name);
};
TypeScriptconst greet = ... の行より前では、greet はまだ存在しません。
これは「変数のホイスティング」と関係していて、const や let は「宣言はされるけど、初期化前には使えない」からです。
つまり、関数式は「その行より後ろ」でしか使えません。
この違いから言えることは、
関数宣言は「ファイル全体で使う“トップレベルの機能”」
関数式は「あるスコープの中で使う“値としての関数”」
という役割分担がしっくりきます。
型の付き方の違い:宣言はシグネチャ、式は「変数の型」として
関数宣言は「その場で型を書く」
関数宣言では、引数と戻り値に直接型を書きます。
function add(a: number, b: number): number {
return a + b;
}
TypeScriptこの書き方はシンプルで読みやすく、
「この関数はこういうものです」と一発で分かります。
また、関数宣言は「関数オーバーロード」とも相性がいいです(後で触れます)。
関数式は「変数の型」として関数型を書く
関数式では、2通りの型の付け方があります。
1つ目は、関数本体に直接書く方法。
const add = function (a: number, b: number): number {
return a + b;
};
TypeScript2つ目は、「変数の型」として関数型を書く方法です。
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 問題も避けやすい。」
コードを書いていて、
「この関数は“どこからどう使われる存在”なんだろう?」
と一度立ち止まって考えてみてください。
その答えが、
関数宣言で書くか、関数式で書くか、
そしてどんな型を付けるか、
という設計の選択につながっていきます。

