【入門編】関数型におけるプロパティ定義:関数をオブジェクトとして扱う – TypeScript コア・型システムの基礎解析バイブル

TypeScriptの「関数オブジェクト」を掌握する:関数は「実行できるオブジェクト」である

こんにちは。TypeScriptの深淵を日々探究しているアーキテクトです。

TypeScriptを学んでいると、ふとこんな疑問が浮かびませんか?
「関数は関数。オブジェクトはオブジェクト。なぜこの2つを混ぜ合わせるような書き方が存在するのか?」

実は、JavaScript/TypeScriptにおいて、「関数は、プロパティを持てる特別なオブジェクト」に過ぎません。この本質を理解すると、ライブラリ設計や複雑な状態管理が劇的に洗練されます。

今日は、関数に関数を生やす、いわゆる「関数オブジェクト」の型定義を、その深層心理まで紐解いていきましょう。

—

1. なぜ「関数にプロパティ」を持たせるのか?

例えば、カウンター関数を作るとしましょう。ただカウントアップするだけでなく、「現在のカウント数をリセットする機能」や「設定値を保持する機能」を持たせたいとします。

ここで、わざわざクラス(Class)を作るほど大袈裟にはしたくない。そんな時に役立つのが「関数オブジェクト」です。

// カウンター関数の型定義
interface Counter {
(initial: number): number; // 関数としてのシグネチャ
count: number; // プロパティとしての定義
reset: () => void; // メソッドとしての定義
}

const createCounter = (): Counter => {
let value = 0;

// 関数本体の定義
const fn = ((initial: number) => {
value += initial;
return value;
}) as Counter; // 型アサーションで無理やり紐付ける

// プロパティを追加
fn.count = value;
fn.reset = () => { value = 0; fn.count = 0; };

return fn;
};

このように、「呼び出し可能(Callable)」でありながら「状態保持可能(Stateful)」という、二面性を持つ設計が可能になります。

—

2. 型定義の極意:インターフェース vs 型エイリアス

この「関数オブジェクト」を定義する際、`interface`を使うべきか、`type`を使うべきか迷う方も多いはずです。

結論から言うと、「拡張性を考慮するなら `interface`」です。

interface を使う場合(推奨)

インターフェースはマージが可能です。後からプロパティを追加したり、別のモジュールで拡張したりする柔軟性があります。

interface MyFunc {
(arg: string): void;
version: string;
}

// 後からプロパティを追加することも可能
interface MyFunc {
author: string;
}

type を使う場合

`type`はシンプルですが、一度定義すると再オープン(拡張)はできません。

type MyFunc = ((arg: string) => void) & {
version: string;
};

現場のアーキテクト視点では、宣言的な読みやすさと拡張性を重視し、`interface`を採用することを強くおすすめします。

—

3. 陥りやすい罠:アサーションの落とし穴

初心者が必ずと言っていいほどハマるのが、「関数定義とプロパティ代入の順序」です。

TypeScriptのコンパイラは、関数定義の時点では「その関数に後からプロパティが生やされること」を知りません。そのため、以下のような書き方はエラーになります。

const myFunc = (x: number) => x 2;
myFunc.prop = “oops”; // ❌ Error: Property ‘prop’ does not exist on type…

【解決策】
一度空のオブジェクトを関数型としてキャストし、そこに値を流し込むアプローチが最も堅牢です。

const myFunc = ((x: number) => x 2) as MyFunc; // 型を先に宣言する
myFunc.prop = “safe”; // ✅ OK

—

4. 現場の知見:なぜこれが「美しい」のか

このパターンの真価は、「APIの直感性」にあります。

例えば、有名なライブラリである `Express` や `JQuery` を想像してみてください。関数をそのまま叩くこともできれば、その関数経由で便利なユーティリティにアクセスできる。

// イメージ
const api = getClient();
api(); // メイン処理を実行
api.config(); // 設定を変更する
api.middleware; // 関連するメタデータにアクセス

このように、「メインの振る舞い」と「付随する設定・状態」をひとつの名前にパッケージングできるのは、関数オブジェクトならではの特権です。

—

まとめ:TypeScriptを掌握するということ

関数オブジェクトの型定義は、一見すると「型のお遊び」のように見えるかもしれません。しかし、「データと振る舞いをどうカプセル化するか」というアーキテクチャの根幹に関わる非常に強力な手法です。

1. `interface` で呼び出しシグネチャとプロパティを定義する
2. 関数を作成した後、型アサーションで型を確定させる
3. 副作用と状態管理をひとつの名前空間に閉じ込める

ここをクリアすれば、あなたはもうただの「コードを書く人」ではありません。「APIを設計するエンジニア」への第一歩を踏み出したと言えるでしょう。

TypeScriptという言語は、制約を与えるものではなく、あなたの設計意図を最も正確に表現するための強力なツールです。ぜひ、今日からあなたの関数を「オブジェクト」として育ててみてください。

それでは、また次回の深淵でお会いしましょう。

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