「関数を変数に代入する」ときに型で何を約束しているか
まず前提をそろえます。
TypeScript では、「関数そのもの」も値として扱えて、変数に代入できます。
function add(a: number, b: number): number {
return a + b;
}
const fn = add; // 関数をそのまま代入している
TypeScriptfn は「関数型の値」です。
ここで言いたいことはただ一つ。
「この変数には“こういう形の関数”が入りますよ」というのを、型で表現できる。
この「こういう形」が、(引数の型たち) => 戻り値の型 という 関数型 です。
それを変数にくっつけるのが「関数を変数に代入する型」です。
基本1:変数に直接「関数型」を書くパターン
具体例で形をつかむ
一番シンプルな書き方から見ます。
const add: (a: number, b: number) => number = function (a, b) {
return a + b;
};
TypeScriptここで注目してほしいのは左側です。
const add: (a: number, b: number) => number
TypeScriptこの部分が「add という変数には、(a: number, b: number) => number 型の関数が入る」と宣言している箇所です。
分解すると、
a: number, b: number
→ この関数はnumberを2つ引数に取る=> number
→ 戻り値はnumber
という約束になっています。
右側の function (a, b) { ... } は、その約束を守る「中身」です。
もし約束を破ると、TypeScript がちゃんと怒ります。
const add: (a: number, b: number) => number = function (a, b) {
return "hello"; // エラー: Type 'string' is not assignable to type 'number'.
};
TypeScriptここで守られているのは、
- 引数の数・型が合っていない関数は代入できない
- 戻り値の型が違う関数も代入できない
という「関数の設計」です。
「変数に関数を入れる」とき、その変数の型が“関数の仕様書”になっている と捉えてください。
基本2:アロー関数を代入するときの型
アロー関数版も考え方は同じ
同じことをアロー関数で書くとこうなります。
const add: (a: number, b: number) => number = (a, b) => {
return a + b;
};
TypeScriptやっていることはまったく同じです。
左側:変数 add は「number, number を受け取って number を返す関数型」
右側:その仕様どおりに作られたアロー関数
なので、引数の型や戻り値を変えようとすると、やはりエラーになります。
const add: (a: number, b: number) => number = (a, b) => {
return `${a + b}`; // エラー
};
TypeScript「アロー関数だろうが function だろうが、変数につける型の書き方は同じ」、これだけ押さえておけばOKです。
パターン3:関数型に type 名をつけてから変数に使う
関数の「形」に名前をつける
何度も同じ形の関数を扱うなら、その「形」に名前を付けると分かりやすくなります。
type AddFn = (a: number, b: number) => number;
const add: AddFn = (a, b) => a + b;
const plus: AddFn = function (x, y) {
return x + y;
};
TypeScriptここでやっているのは、
type AddFn = (a: number, b: number) => number;
→ 関数型そのものにAddFnというラベルをつけたconst add: AddFn = ...;/const plus: AddFn = ...;
→ どちらも「AddFn 型の関数」を入れる変数
という構造です。
このスタイルの良さは、
- 同じ型をあちこちで再利用できる
- 引数の名前は違ってもOK(位置と型が合っていれば同じ関数型)
という点です。
const sum: AddFn = (x, y) => x + y; // a, b じゃなくてもOK
TypeScript「まず type で“こういう関数が欲しい”と定義し、それを変数にくっつける」
実務での定番パターンなので、早めに慣れておくとかなり楽になります。
パターン4:関数を受け取る変数(コールバック型)にも同じ考え方を使う
例:文字列を加工する関数を受け取る変数
「関数を変数に代入する型」は、そのまま「コールバック関数の型」にもなります。
type StringFn = (value: string) => string;
const toUpper: StringFn = (v) => v.toUpperCase();
const withMark: StringFn = (v) => v + "!";
function run(fn: StringFn) {
console.log(fn("hello"));
}
run(toUpper); // "HELLO"
run(withMark); // "hello!"
TypeScriptここでは、
StringFnが「string を受け取って string を返す関数型」toUpper/withMarkは「その型の関数を代入した変数」runの引数fn: StringFnにも使っている
という状態になっています。
「関数型を一度定義しておけば、“関数を入れる変数”にも、“関数を受け取る引数”にも、同じ型を貼れる」
これがかなり強いです。
パターン5:オブジェクトのプロパティとして関数を持つ場合の型
メソッドの形も「関数を変数に代入する型」と同じ
オブジェクトの中で関数をプロパティとして持つときも、実態は「プロパティに関数を代入している変数」と同じです。
type Logger = {
log: (message: string) => void;
};
const consoleLogger: Logger = {
log: (message) => {
console.log("[LOG]", message);
},
};
consoleLogger.log("hello");
TypeScriptlog: (message: string) => void;
この部分が、「log というプロパティには、こういう形の関数が入ります」という宣言です。
実際に代入するのはアロー関数だったり function 式だったりしますが、型の考え方は変わりません。
「関数を変数に代入する型」を設計するときの考え方
ここが一番大事な部分です。
記法を覚えるより、「どう考えればいいか」を身体に入れてほしい。
関数を変数に入れるとき、必ず自分に問いかけます。
この変数に入ってほしい関数は、どんな引数を取るべき?
それぞれの引数は、どんな型?
その関数は、何を返すべき?(何も返さないなら void)
例えば「ユーザーをIDから読み込む関数」を変数にしたいなら、頭の中にはこういうイメージがあるはずです。
ID(string)を1つ受け取る
非同期にユーザー(User 型)を返す(Promise<User>)
それをそのまま型にすると、こうなります。
type LoadUser = (id: string) => Promise<User>;
const loadUserFromApi: LoadUser = async (id) => {
const res = await fetch(`/api/users/${id}`);
return res.json();
};
TypeScript「頭の中の“こういう関数が欲しい”というイメージを、そのまま (引数) => 戻り値 という形に言語化する」
これができるようになると、関数を変数に代入するときの型付けはほぼ制覇です。
まとめ:関数を変数に代入する型は「関数用の型エイリアス」
最後に、感覚だけ整理します。
関数も値なので、変数に代入できる。
その変数に「この形の関数しか入れてほしくない」と言いたいとき、(arg1: 型1, arg2: 型2) => 戻り値の型 という関数型を付ける。
よく使う形は type FnName = (args) => return; と型エイリアスにして、変数・引数・プロパティに再利用する。
そして一番大事なのは、型を書く前にいつも自分に問いかけることです。
「この変数に入る“関数”は、本当はどんな役割を持っているんだっけ?」
その答えを、関数型として書き下ろしたものが、
「関数を変数に代入する型」です。
