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という言語は、制約を与えるものではなく、あなたの設計意図を最も正確に表現するための強力なツールです。ぜひ、今日からあなたの関数を「オブジェクト」として育ててみてください。
それでは、また次回の深淵でお会いしましょう。