Decorator — TC39 Decorators とは別物のパターン
同名の言語機能との区別を最初に済ませ、高階関数との使い分けを扱います。
TypeScript には @log のように書く Decorators という言語機能があります。本章が扱う GoF の Decorator パターンとは別の概念です。名前が同じなだけで、解こうとしている問題も書き方も違います。
- 言語機能の Decorators — クラスやメンバーに注釈を付けて、その定義を再利用可能な形で加工する構文です。ECMAScript への提案 (2026 年 8 月時点で Stage 2.7) に基づく構文で、まだ言語仕様には入っていません1。TypeScript 5.0 以降はコンパイラのフラグなしで書けます2。本サイトでは クラスと継承 が旧実装との違いを含めて扱っています。
- GoF の Decorator パターン — オブジェクトを同じ形の別のオブジェクトで包み、外から見た形を変えずに振る舞いを足す設計手法です。構文の話ではありません。
言語機能の Decorators を使って GoF の Decorator パターンを書くこともできますが、そうしなければならないわけではありません。名前が重なっているだけで、片方を使わずにもう片方を書けます。本章は後者だけを扱います。
このパターンが解こうとした問題
GoF は Decorator を、既存のオブジェクトの振る舞いを動的に追加または上書きするパターンとして収録しています3。
機能の組み合わせを継承で表そうとすると、組み合わせの数だけクラスが要ります。再試行あり・ログあり・キャッシュありの 3 つの機能なら、少なくとも 1 つを持つ組み合わせは 7 通りあり、その数だけクラスが要ります。Decorator はこれを、同じ形を持つ包み紙を重ねることで解きます。包む順番を変えれば組み合わせが変わるので、クラスの数は機能の数だけで済みます。
クラスで素直に書く
JSON を取ってくる処理を題材にします。共通の形を決めて、包む側もその形を実装します。
interface JsonSource {
load(url: string): Promise<unknown>;
}
class HttpJsonSource implements JsonSource {
async load(url: string): Promise<unknown> {
const response = await fetch(url);
return response.json();
}
}
// 包む側の土台。中身を 1 つ抱え、自分も同じ形を実装する
// 素通しはここに 1 回だけ書く。具象デコレータは変えたいものだけ上書きする
class JsonSourceDecorator implements JsonSource {
constructor(protected readonly inner: JsonSource) {}
load(url: string): Promise<unknown> {
return this.inner.load(url);
}
}
class RetryingJsonSource extends JsonSourceDecorator {
constructor(
inner: JsonSource,
private readonly attempts: number,
) {
super(inner);
}
override async load(url: string): Promise<unknown> {
let lastError: unknown = new Error('attempts must be >= 1');
for (let i = 0; i < this.attempts; i++) {
try {
return await this.inner.load(url);
} catch (error) {
lastError = error;
}
}
throw lastError;
}
}
class LoggingJsonSource extends JsonSourceDecorator {
override async load(url: string): Promise<unknown> {
const startedAt = performance.now();
try {
return await this.inner.load(url);
} finally {
console.log(`${url}: ${Math.round(performance.now() - startedAt)}ms`);
}
}
}
// 包む順番で組み合わせが決まる
const source: JsonSource = new LoggingJsonSource(
new RetryingJsonSource(new HttpJsonSource(), 3),
);
意図どおりに動きます。ただし、実際に振る舞いを書いている行は load の中身だけで、残りはクラス宣言・コンストラクタ・super の受け渡しに費やされています。
言語機能で置き換える
包む対象が関数 1 つなら、包み紙も関数で書けます。関数を受け取って関数を返す関数、つまり高階関数です。
type JsonFetch = (url: string) => Promise<unknown>;
const httpFetch: JsonFetch = async (url) => {
const response = await fetch(url);
return response.json();
};
function withRetry(inner: JsonFetch, attempts: number): JsonFetch {
return async (url) => {
let lastError: unknown = new Error('attempts must be >= 1');
for (let i = 0; i < attempts; i++) {
try {
return await inner(url);
} catch (error) {
lastError = error;
}
}
throw lastError;
};
}
function withLogging(inner: JsonFetch): JsonFetch {
return async (url) => {
const startedAt = performance.now();
try {
return await inner(url);
} finally {
console.log(`${url}: ${Math.round(performance.now() - startedAt)}ms`);
}
};
}
// 入れ子の形はクラスの版とそのまま同じ
const load: JsonFetch = withLogging(withRetry(httpFetch, 3));
入れ子の構造も、包む順番が意味を持つことも変わりません。減ったのは、interface の宣言、抽象クラス、コンストラクタ、super の受け渡しです。包み紙の骨組みだけがなくなり、振る舞いの部分はそのまま残っています。
本連載の判断
判定: 条件付き。包む対象が持つメソッドの数で分かれます。
- メソッドが 1 つ (実質的に関数) なら高階関数 — 上の書き換えがそのまま当てはまります。型は
JsonFetchのような関数型 1 つで足り、包み紙ごとの型宣言も要りません。 - メソッドが複数ある契約を包むならクラスかオブジェクト — 関数合成では、契約に含まれるメソッドを 1 つずつ手で通すことになります。5 つのメソッドのうち 1 つだけに手を入れたい場合でも、残り 4 つを素通しさせる記述が要ります。クラスなら、素通しを基底のデコレータクラスに 1 回だけ書けば、以降の具象デコレータは継承で済みます。総量がゼロになるわけではなく、包み紙の数だけ繰り返さずに済む、という違いです。
判断の分かれ目は「振る舞いを足したいか」ではなく、**「素通しさせる必要のあるメソッドが何個あるか」**です。素通しの記述が本来の変更点より多くなるなら、クラスのほうが読みやすくなります。
この判定が覆る条件
素通しの記述は、組み込みの Proxy を使えば動的に処理できます。これを使うとメソッドが何個あっても包み紙は 1 つで済むので、上の判断が変わります。ただし Proxy では、横取りした先で返す値の型検査が一部効きません。この扱いは Proxy で扱います。
もう 1 つ、高階関数で包むと 元の関数の名前がスタックトレースから消えることがあります。障害調査で呼び出し元をたどる必要が強い箇所では、この点が実務上の判断材料になります。
既存記事との関係
- クラスと継承 が言語機能としての Decorators を扱っています。新しい実装と旧実装 (
--experimentalDecorators) の違い、Symbol.metadata、Angular や NestJS など旧実装を必要とするフレームワークの状況まで、そちらが正本です。本章は同じ語の別概念を扱っているので、内容は重なりません。なお提案の段階については、本章の脚注が示す TC39 の README を一次ソースとしてください。 - クラスとインターフェース が継承より合成を優先する考え方を扱っています。Decorator は合成で機能を積む代表例にあたります。