TypeScript | 関数・クラス・ジェネリクス:関数設計の深化 – 関数型interfaceの書き方

TypeScript
スポンサーリンク

前提:関数にも「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[]
TypeScript

interface の中に「複数の 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 は「書き方のバリエーション」から、
設計意図をそのまま型に刻むための、強力な道具 に変わっていきます。

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