「関数式の型」とは何か
まず言葉の整理からいきます。
TypeScript で「関数式の型」といったとき、だいたい次のようなものを指します。
const add = function (a: number, b: number): number {
return a + b;
};
TypeScriptこの function (a: number, b: number): number { ... } の部分は「関数式」です。
これを変数 add に代入しています。
つまり、
- 関数「定義」:
function add(a: number, b: number) { ... } - 関数「式」 :
const add = function (a: number, b: number) { ... }
という違いです。
そして「関数式の型」とは、
「この変数には、こういう引数と戻り値を持つ“関数”が入りますよ」という型のことです。
「数じゃなくて“関数そのもの”を変数に入れている」
そのときに、その「関数の形」に型をつける——これが関数式の型の話です。
一番基本:関数式に型を書く2つの場所
パターン1: 関数式の中に引数型・戻り値型を書く
いちばん素直な書き方はこれです。
const add = function (a: number, b: number): number {
return a + b;
};
const result = add(1, 2); // result: number
// add("1", 2); // エラー
TypeScriptここでは、
a: number, b: numberが「引数の型」): numberが「戻り値の型」
という意味を持っています。
関数宣言とまったく同じ考え方ですが、
「名前付き関数」ではなく「無名関数(function の後に名前がない)」を変数に入れている形ですね。
関数式側にだけ型を書くスタイルなので、
変数 add には TypeScript が「(a: number, b: number) => number 型」と自動でつけてくれます。
パターン2: 変数側に「関数型」を書いてから、関数式を代入する
実務でよく使われるのはこちらのパターンです。
const add: (a: number, b: number) => number = function (a, b) {
return a + b;
};
TypeScript左側の : (a: number, b: number) => number が「関数の型」を表しています。
右側の function (a, b) { ... } は、その型に合う「実装」です。
ポイントは、
- 変数の型として「関数の形」を宣言する
- その宣言に合わない関数を代入しようとするとエラーになる
ということです。
たとえば、戻り値を返し忘れるとこうなります。
const add: (a: number, b: number) => number = function (a, b) {
// return a + b; を書き忘れた
};
// エラー: Type 'void' is not assignable to type 'number'.
TypeScript「左で“こういう関数が欲しい”と宣言し、右に“その仕様を満たす関数”を書く」
このスタイルが「関数式の型」を活かした書き方です。
関数型を type で名前を付けてから使う
関数の形に「ラベル」をつける
同じ形の関数が何回も出てくるなら、その「関数の型」に名前をつけておくとすごく楽になります。
type Add = (a: number, b: number) => number;
const add1: Add = function (a, b) {
return a + b;
};
const add2: Add = (a, b) => a + b;
TypeScriptここでやっていることは、
type Add = (a: number, b: number) => number;→ 「2つの number を受け取って number を返す関数型」に Add という名前をつけるconst add1: Add = ...;const add2: Add = ...;→ 「どちらも Add 型の関数である」と宣言
です。
これの何がいいかというと、
同じ関数型を別の場所でも再利用できる
引数名が違っても、型(位置と型)が合っていれば OK
という点です。
const plus: Add = function (x, y) {
return x + y; // OK
};
TypeScript「名前は a, b じゃなきゃダメ」ということはなく、
「順番と型が一致していれば同じ関数型」として扱われます。
関数式の型を「引数」として使う(コールバック)
関数を受け取る関数
関数式の型が真価を発揮するのは、「関数を引数に取る」場面です。
type StringProcessor = (value: string) => string;
function processAndPrint(processor: StringProcessor) {
const input = "hello";
const output = processor(input);
console.log(output);
}
TypeScriptここで StringProcessor は、「string を受け取って string を返す関数型」です。
使う側は、関数式(やアロー関数)を渡します。
const toUpper: StringProcessor = function (value) {
return value.toUpperCase();
};
processAndPrint(toUpper); // "HELLO"
processAndPrint((v) => v + "!"); // "hello!"
TypeScriptここで守られているのは、
processAndPrintに渡せるのは、「string を受け取り string を返す関数」だけ- 他の型の引数を取る関数や、戻り値が違う関数を渡そうとするとエラー
ということです。
「関数を“値”としてやりとりするとき、その関数の“形”にも型を付ける」
これが、関数式の型を理解するうえで一番大事なポイントです。
関数式の型推論と「どこまで書くか」のバランス
TypeScript が推論してくれる場合
たとえば、配列の map を使うとき。
const numbers = [1, 2, 3];
const doubled = numbers.map(function (n) {
return n * 2;
});
TypeScriptここで n の型は、numbers が number[] であることから自動で number と推論されます。function (n: number) とわざわざ書かなくても、型安全です。
「呼び出し元(map 側)が関数型を知っている」場合は、関数式側の型は省略してよい、というのが実務での感覚です。
自分で変数に入れるときはどうするか
一方で、自分で変数に関数を入れるときは、次のように考えます。
すぐ下でしか使わない小さな関数 → 関数式側にだけ型を書く or 推論に任せる
他の場所からも使われる関数 → 変数側に「関数型」を明示しておく
たとえば、サービス関数のように「いろんなところから呼ばれる関数」は、こうしたくなります。
type FetchUser = (id: string) => Promise<User>;
const fetchUser: FetchUser = async function (id) {
// ...
};
TypeScript「この変数は、こういう関数であるべき」という約束を型で固定する
この意識を持てるようになると、関数式の型が一気に“設計の道具”になります。
「関数の型」をどう言語化するかが上達の鍵
関数式の型を考えるとき、いつも同じ問いに戻ってきます。
この関数は、どんな引数を受け取るべきか?
それぞれの引数は、どんな型をしているべきか?
この関数を呼んだとき、呼び出し側はどんな型の値を受け取れるべきか?
その答えを、(arg1: 型1, arg2: 型2) => 戻り値の型
という形で素直に書き下す。
それを変数にくっつければ、それがそのまま「関数式の型」になります。
type Logger = (message: string, level?: "info" | "warn" | "error") => void;
const log: Logger = function (message, level = "info") {
console.log(`[${level}] ${message}`);
};
TypeScriptこの例だと、
- message は string
- level は省略可能で、指定するなら “info” | “warn” | “error”
- 戻り値は使わない(void)
という「関数の設計」が、そのまま型に刻まれています。
「関数の“意図”を、型として言葉にする」
その感覚がついてくると、「関数式の型」は一気に楽しくなります。
