前提:関数にも「interface で形を決める」という発想がある
まず押さえたいのは、
「関数の型は type だけじゃなく、interface でも書ける」
ということです。
type FnByType = (x: number) => number;
interface FnByInterface {
(x: number): number;
}
TypeScriptどちらも「number を受け取って number を返す関数」という意味ですが、interface で書くときは
interface 名前 {
(引数): 戻り値;
}
TypeScriptという「呼び出しシグネチャ(call signature)」の形になります。
ここから、「関数型 interface ならではの強み」を、例を通して深掘りしていきます。
基本形:関数型 interface の最小パターン
ただの「関数の形」を interface で書く
一番シンプルな関数型 interface はこうです。
interface StringToNumber {
(value: string): number;
}
const length: StringToNumber = (value) => value.length;
const toNumber: StringToNumber = (value) => Number(value);
TypeScriptここでのポイントは2つです。
1つ目は、「interface の中に“名前のない関数”を書く」という感覚。(value: string): number; だけで、「この interface は“こう呼べる”」と定義できます。
2つ目は、「代入側は普通の関数」でいいこと。
アロー関数でも function 宣言でも構いません。
const f1: StringToNumber = function (v) {
return v.length;
};
const f2: StringToNumber = (v) => v.length;
TypeScript「関数の形を interface で決めておいて、実装は好きな書き方で」という分離ができるのが気持ちいいところです。
関数型 interface が「type より得意」な場面1:関数+プロパティを持つ形
「呼べるし、プロパティも持っているオブジェクト」を表現する
type でもできますが、interface の方がしっくり来る典型パターンがこれです。
「関数として呼べるし、同時に設定プロパティも持っているもの」。
interface Logger {
(message: string): void; // 関数として呼べる
prefix: string; // プロパティも持つ
}
const logger: Logger = (message: string) => {
console.log(logger.prefix + message);
};
logger.prefix = "[INFO] ";
logger("起動しました"); // [INFO] 起動しました
TypeScriptここでやっていることは、
「Logger という“呼べるオブジェクト”の形を interface で定義している」
ということです。
関数型エイリアス(type)だと、こう書きがちです。
type LoggerBad = (message: string) => void;
// これだと prefix プロパティを足せない
TypeScriptもちろん、type でも
type Logger = {
(message: string): void;
prefix: string;
};
TypeScriptと書けますが、「オブジェクトの形」を表すときは interface の方が自然に感じる場面が多いです。
ここでの重要ポイントは、
「関数型 interface は、“関数として呼べるオブジェクト”を表現するのに向いている」
ということです。
関数型 interface が「type より得意」な場面2:オーバーロード(複数の呼び出しパターン)
「引数のパターンによって戻り値が変わる関数」を interface で書く
例えば、parse という関数を考えます。
文字列を渡すと number を返す
配列を渡すと number[] を返す
という仕様にしたいとします。
interface Parse {
(value: string): number;
(value: string[]): number[];
}
const parse: Parse = (value: string | string[]) => {
if (Array.isArray(value)) {
return value.map((v) => Number(v));
}
return Number(value);
};
const n = parse("10"); // n: number
const arr = parse(["1", "2"]); // arr: number[]
TypeScriptinterface の中に「複数の call signature」を書くことで、
「オーバーロードされた関数の型」を表現できます。
これを type で書こうとすると、少し読みにくくなります。
type Parse =
| ((value: string) => number)
| ((value: string[]) => number[]);
TypeScriptこの書き方だと、「どっちの関数なのか」が union になってしまい、
実装側で扱いづらくなります。
interface のオーバーロードは、
interface Parse {
(value: string): number;
(value: string[]): number[];
}
TypeScriptと「1つの関数に複数の顔を持たせる」イメージで書けるのが強みです。
ここでの重要ポイントは、
「複数の呼び出しパターンを持つ関数は、interface の call signature を並べるときれいに書ける」
ということです。
ジェネリクス付き関数型 interface:パターンを抽象化する
「T を受け取って U を返す関数」という形に名前をつける
関数型エイリアスと同じように、interface でもジェネリクスを使えます。
interface Transformer<T, U> {
(input: T): U;
}
const stringToNumber: Transformer<string, number> = (value) => Number(value);
const numberToString: Transformer<number, string> = (value) => value.toString();
TypeScriptここでは、
「T を受け取って U を返す関数」
というパターンを Transformer<T, U> という名前で表現しています。
さらに、高階関数にも使えます。
function withLog<T, U>(fn: Transformer<T, U>): Transformer<T, U> {
return (input: T) => {
console.log("input:", input);
const result = fn(input);
console.log("result:", result);
return result;
};
}
TypeScriptここでのポイントは、
「ジェネリクス付き関数型 interface は、“関数の形のパターン”を表現するのに向いている」
ということです。
Transformer<T, U> という名前を見ただけで、
「T を U に変換する関数なんだな」と分かります。
関数型 interface とクラスの相性:実装をクラスに任せる
「このクラスは“関数として呼べる”」という設計
実務ではあまり多くないですが、
「クラスに関数型 interface を実装させる」こともできます。
interface Adder {
(x: number, y: number): number;
}
class AddImpl {
call(x: number, y: number): number {
return x + y;
}
}
// 実際に「クラスインスタンスを関数として呼ぶ」ことはできないので、
// ここはラッパーを作るイメージで捉えてください。
const adder: Adder = (x, y) => new AddImpl().call(x, y);
TypeScript「クラスに直接 call signature を実装して、インスタンスを関数として呼ぶ」
というのは JavaScript の仕様上できませんが、
「関数型 interface で“こういう関数が欲しい”と決めておいて、
その実装をクラスに任せる」
という設計はよくあります。
例えば、DI(依存性注入)やテストダブルの差し替えなどで、
「この処理は RequestHandler という関数型 interface を満たしていれば何でもいい」
という書き方ができます。
interface RequestHandler {
(path: string): Promise<string>;
}
class ApiClient implements RequestHandler {
async handle(path: string): Promise<string> {
// 本当は HTTP リクエストなど
return `GET ${path}`;
}
}
// 実際には「関数として渡すラッパー」を用意することが多い
const handler: RequestHandler = (path) => new ApiClient().handle(path);
TypeScriptここでのポイントは、
「関数型 interface は、“この処理はこういう関数として扱う”という契約を表現できる」
ということです。
関数型 interface の書き方で意識してほしい設計ポイント
「これは“関数そのもの”か、“呼べるオブジェクト”か」を意識する
関数型を設計するとき、まず自分にこう聞いてみてください。
「これは、ただの関数の形だけあればいい?」
「それとも、“関数として呼べて、プロパティも持つオブジェクト”として扱いたい?」
ただの関数なら、type でも interface でもどちらでも構いません。
好みで選んでOKです。
でも、
設定を持つロガー
状態を持つハンドラ
複数の呼び出しパターンを持つ関数
のように、
「関数+α(プロパティやオーバーロード)」が欲しくなった瞬間に、
関数型 interface が本領発揮します。
もう1つの問いはこれです。
「この関数の“形”に名前をつけたら、コードの意図が読みやすくなるか?」
(value: string) => void より MessageHandler(value: T) => boolean より Predicate<T>
の方が、圧倒的に「何をする関数か」が伝わります。
関数型 interface は、
「関数の形に“意味のある名前”を与えるための器」
だと捉えておくと、設計の判断がしやすくなります。
まとめ:関数型 interface の書き方を自分の言葉で言うと
最後に、あなた自身の言葉でこう整理してみてください。
関数型 interface は、
interface 名前 {
(引数): 戻り値;
}
TypeScriptという「call signature」で書く。
必要なら、その中にプロパティや複数の call signature も書ける。
ただの関数の形だけなら type でもいいけれど、
呼べるオブジェクト(関数+プロパティ)
複数の呼び出しパターン(オーバーロード)
ジェネリクスで表現したい関数パターン(Transformer など)
を扱いたいとき、関数型 interface は特に力を発揮する。
コードを書くとき、
「これはただの関数か? それとも“呼べる何か”として設計したいのか?」
「この関数の形に名前をつけたら、読み手は楽になるか?」
と一度自分に問いかけてから、interface で書くかどうかを決めてみてください。
その一呼吸で、
関数型 interface は「書き方のバリエーション」から、
設計意図をそのまま型に刻むための、強力な道具 に変わっていきます。
