こんにちは。テクニカルリードの私だ。
コードレビューをしていると、未だに以下のようなコードに遭遇する。
// よく見る、だが実務では脆弱なコード
async function processUserData(fetcher: () => Promise
const user = await fetcher();
// 何らかの処理…
return user;
}
「おや、一見して問題なさそうだが?」と思った君。TypeScriptの型システムとJavaScriptのランタイムの非同期境界を、まだ表面しか見えていない証拠だ。
この `Promise
今回は、TypeScriptの型システムを極限までハックし、あらゆる「非同期の萌芽」を安全に飲み込むための `PromiseLike` を用いた堅牢な関数型設計について、コンパイラの挙動から実務コードまで徹底的に解説しよう。
—
1. なぜ `Promise` ではなく `PromiseLike` なのか?
TypeScriptにおける `Promise
これを型レベルで表現するのが、TypeScript標準ライブラリ(`lib.es5.d.ts`)に定義されたインターフェース、`PromiseLike
interface PromiseLike
then
onfulfilled?: ((value: T) => TResult1 | PromiseLike
onrejected?: ((reason: any) => TResult2 | PromiseLike
): PromiseLike
}
コンパイラ視点での違い
- `Promise
` : `async/await` のネイティブサポートや、`.catch()`, `.finally()` などの豊富なメソッドチェーンを持つコンクリートな型。 - `PromiseLike
` : 「`then` さえ生えていれば、それは非同期タスクである」とみなす、構造的型付け(Structural Subtyping)の極み。
関数の引数や戻り値の受け渡しにおいて、「入力はより広く(Contravariant)、出力はより狭く(Covariant)」受け入れるべきという原則に基づけば、非同期関数が受け取るデータソースは `Promise
—
2. 実務で直面するアンチパターンと設計の課題
例えば、フロントエンドのデータフェッチ層や、BFF(Backend for Frontend)のクライアントSDKを設計している場面を想像してほしい。
// 悪い例:具象クラスに依存したタイトカップリングな設計
class ApiClient {
async get
const res = await fetch(url);
return res.json();
}
}
// テスト時にモックや独自キャッシュ機構(Promiseではないがthenableなカスタムオブジェクト)を
// 差し込もうとした瞬間に型エラーの嵐に見舞われる。
ここで `Promise
—
3. プロダクションコード:`PromiseLike` を極めた汎用非同期パイプライン
では、実務の現場でそのまま使える、極限まで柔軟かつ厳格な型安全性を誇る非同期ユーティリティ関数の実装を見ていこう。
以下のコードは、任意の非同期タスク(`Promise` でも、独自の `Thenable` でも、同期的な値ですら)を受け取り、安全にハンドリングして返すパイプラインの設計例だ。
/
- 任意の非同期値、あるいは同期的な値を安全にラップし、
- 最終的に標準の Promise
として解決するロバストなパイプライン関数。
/
// T に加え、Thenable である PromiseLike も許容するユーティリティ型
type AsyncInput
/
- 実行時に関数、または非同期値を受け取り、安全に解決する高階関数
/
export async function executeAsyncOperation
inputSource: AsyncInput
transform: (val: Awaited
): Promise
try {
// 1. 入力が関数であれば実行し、でなければそのまま評価
const rawInput = typeof inputSource === ‘function’
? (inputSource as () => AsyncInput
: inputSource;
// 2. PromiseLike かどうかをコンパイル時・実行時で安全に解決
// Awaited
const resolvedInput = await rawInput;
// 3. トランスフォームの適用
const rawOutput = transform(resolvedInput as Awaited
// 4. 出力側も PromiseLike の可能性を考慮して await
const finalOutput = await rawOutput;
return finalOutput as Awaited
} catch (error: unknown) {
// 厳格なエラーハンドリング(unknown 型ガードの活用)
if (error instanceof Error) {
throw new Error(`[AsyncPipelineError]: Failed to execute operation -> ${error.message}`);
}
throw new Error(‘[AsyncPipelineError]: An unknown non-Error rejection occurred.’);
}
}
このコードのアーキテクチャ的優位性
1. `Awaited
TypeScript 4.5以降で導入された `Awaited
2. 遅延評価(Lazy Evaluation)への対応
引数に `AsyncInput
3. ランタイムと型定義の完全な同期
`await` キーワードは、ランタイムにおいてネイティブの `Promise` だけでなく、仕様を満たすすべての `PromiseLike`(`thenable`)オブジェクトをネイティブプロミスに昇格させる特性を持っている。この関数はそのJavaScriptの仕様を型レベルで正確にトレースしている。
—
4. 実戦投入:カスタムThenableとの統合例
この設計がどれほど強力か、実際のユースケースを見てみよう。以下は、標準の `Promise` ではなく、独自のキャッシュ機構を持つカスタム `Thenable` オブジェクトをこの関数に流し込む例だ。
// 独自のキャッシュ付き Thenable オブジェクト(Promiseクラスを継承していない)
class SimpleCacheThenable
constructor(private readonly data: T) {}
// then メソッドを持つため、TypeScriptの型システム上もランタイム上も PromiseLike として扱える
then
onfulfilled?: ((value: T) => TResult1 | PromiseLike
onrejected?: ((reason: any) => TResult2 | PromiseLike
): PromiseLike
try {
if (onfulfilled) {
// 同期的に解決をシミュレート
const result = onfulfilled(this.data);
// 簡易的な Thenable の返却
return new SimpleCacheThenable(result as any) as any;
}
return this as any;
} catch (error) {
if (onrejected) {
const errorResult = onrejected(error);
return new SimpleCacheThenable(errorResult as any) as any;
}
throw error;
}
}
}
// — 実行と検証 —
async function run() {
// 1. 通常の値を渡す
const res1 = await executeAsyncOperation(42, (val) => val 2);
console.log(res1); // 84 (型は Promise
// 2. ネイティブの Promise を渡す
const res2 = await executeAsyncOperation(
Promise.resolve(“hello”),
(val) => val.toUpperCase()
);
console.log(res2); // “HELLO” (型は Promise
// 3. 【真骨頂】カスタムの Thenable(Promiseではない)を渡す!
const cacheObj = new SimpleCacheThenable(100);
const res3 = await executeAsyncOperation(
cacheObj,
(val) => Promise.resolve(val + 50) // 内部で Promise を返す変換も完璧に型推論される
);
console.log(res3); // 150 (型は Promise
}
run();
コンパイルエラーは一切起きず、IDEのインテリセンスはすべてのステップで正確な型(`number` や `string`)を完全に追跡し続ける。これが、型定義を徹底的に抽象化したアーキテクチャの美しさだ。
—
テクニカルリードからの総括
初心者は `Promise
フロントエンドの複雑性が増し、ReactのSuspenseやServer Components、サードパーティのデータフェッチライブラリが複雑に絡み合う現代のWeb開発において、関数の引数や戻り値を具象的な `Promise` に固定化することは、将来の拡張性を自らブチ折る行為に等しい。
「非同期処理を扱う関数を書くときは、常にその入力が『Thenable』であり得ることを想定せよ」
この知見をコードベースに浸透させれば、君のチームのコードは、いかなる変更や外部ライブラリの差し替えに対しても揺るぎない、極限まで堅牢なシステムへと昇華されるはずだ。さっそく今日のプルリクエストから、型定義を見直してみたまえ。