【入門編】関数型における「PromiseLike」を用いた非同期関数の柔軟な型定義 – TypeScript コア・型システムの基礎解析バイブル

こんにちは!TypeScriptの世界へようこそ。
フロントエンドからバックエンドまで、型安全で頑健なアプリケーションを作る上で、非同期処理の扱いは避けて通れない大きなテーマですよね。

今回は、TypeScriptにおける非同期関数の型定義をワンランク引き上げる、`PromiseLike`を用いた超実践的なアプローチについて解説していきます。

「Promiseなら普段から使ってるよ」という方も多いと思いますが、そこから一歩踏み込んで「Thenable(セナブル)なオブジェクトとは何か?」「なぜ単なる`Promise`ではなく`PromiseLike`を使うべきなのか?」その本質をマスターすると、あなたの書くコードの柔軟性と再利用性が劇的に跳ね上がりますよ。

ここをクリアすれば、TypeScriptの非同期型システムの本質はバッチリマスターできます。一緒に優しく紐解いていきましょう!

—

1. そもそも「Promise」と「PromiseLike」は何が違うの?

TypeScriptで非同期処理を扱うとき、私たちは無意識のうちに次のような型定義を書いています。

async function fetchUserData(userId: string): Promise {
// …
}

この `Promise` は、「最終的に `User` 型の値を返す約束手形」を表しています。

しかし、現代のJavaScriptエコシステムでは、必ずしもJavaScript標準の `Promise` クラスのインスタンスだけが非同期の値として扱われるわけではありません。

  • 独自の非同期ライブラリが返すカスタムのPromise風オブジェクト
  • RxJSなどのObservableの一部機能
  • 外部ライブラリが提供する遅延評価オブジェクト

これらに共通しているのは、「`.then()` メソッドを持っている」という点です。TypeScriptでは、この「`.then()` さえ持っていれば非同期処理っぽく扱えるオブジェクト」の総称を `Thenable` と呼び、それを型として表現したものが `PromiseLike` です。

図解イメージ:包容力の違い

[標準の Promise]
└─ Promise クラスを継承しているガチガチのオブジェクト

[PromiseLike]
└─ 「.then()さえ持ってれば、中身が何であれ受け入れるよ!」という懐の深いインターフェース

TypeScriptの型システムにおいて、`Promise` は `PromiseLike` の部分型(subtype)です。つまり、`Promise` を受け取れる場所には、もちろん通常の `Promise` も渡すことができます。

—

2. 実践:`PromiseLike` を使った柔軟な関数型定義

では、実際に具体的なコードを見てみましょう。
ここでは、「受け取った非同期の処理(あるいは値)を安全に解決し、ログを出力しつつ結果を加工する」という汎用ロガー関数を作ってみます。

/

  • Promiseだけでなく、thenableなオブジェクトも受け取れる汎用ロガー関数
  • @template T 解決される値の型
  • @param asyncValue – Promise、または .then を持つオブジェクト、あるいは普通の直値

/
async function processAsyncValue(asyncValue: PromiseLike | T): Promise {
console.log(“処理を開始します…”);

// PromiseLike | T 型であっても、await を使えば安全に解決できる!
const resolvedValue = await asyncValue;

console.log(“解決された値:”, resolvedValue);
return resolvedValue;
}

このコードの何が凄いの?

引数の型に注目してください。`Promise` ではなく `PromiseLike | T` になっていますよね。

これにより、この関数は以下の3つのパターンの引数を完全に型安全に受け取ることができます。

1. 通常の `Promise` (`Promise.resolve(42)`)
2. サードパーティ製の Thenable オブジェクト (`{ then: (onFulfilled) => onFulfilled(42) }`)
3. 非同期ですらない普通の直値 (`42`)

TypeScriptの `await` 演算子は非常に優秀で、対象が `PromiseLike` であろうが、ただのプリミティブな値であろうが、自動的にラップして同期的に(あるいは正しくマイクロタスクとして)解決してくれます。

—

3. 陥りやすい文法エラーと注意点

ここで、初学者がよくやってしまう罠と、その回避方法についても触れておきましょう。

罠1: 単に `any` で受けてしまう

「動けばいいや」と、ついつい引数を `any` や `unknown` で受けてしまいがちです。

// ❌ アンチパターン:型情報が完全に失われてしまう
async function badProcess(value: any) {
return await value;
}

これではTypeScriptを使っている意味がありません。戻り値の型が推論できなくなり、呼び出し元で型補完の恩恵を受けられなくなってしまいます。ジェネクス(``)と `PromiseLike` を組み合わせて、型安全性を1ミリも妥協しないのがプロの作法です。

罠2: `.then` を直接呼び出そうとして複雑化する

`PromiseLike` を手動で処理しようとして、 `.then()` をチェーンさせまくると、コードがいわゆる「コールバック地獄(then地獄)」のようになりがちです。

// ❌ 複雑になりがちな書き方
function legacyProcess(value: PromiseLike): Promise {
return new Promise((resolve, reject) => {
value.then(
(res) => resolve(res),
(err) => reject(err)
);
});
}

正解の処方箋:
TypeScript / JavaScriptの現代的な開発では、`PromiseLike` を受け取ったら、自前で `new Promise` を書くのではなく、`await` 演算子にそのまま渡すのが最もシンプルで、かつコンパイラにとっても最適なコード生成がされるベストプラクティスです。

—

4. 動作確認:実際に動かしてみよう

以下のコードをTypeScriptのプレイグラウンドや手元の環境で実行してみてください。すべて美しく型推論され、コンパイルが通ります。

// パターンA: 普通のPromiseを渡す
const standardPromise = Promise.resolve(“Hello, Promise!”);
processAsyncValue(standardPromise);

// パターンB: 自作の Thenable オブジェクトを渡す
const customThenable = {
then(onFulfilled?: (value: string) => void) {
if (onFulfilled) {
onFulfilled(“Hello, Custom Thenable!”);
}
}
};
processAsyncValue(customThenable);

// パターンC: 非同期ですらない普通の値を渡す
const directValue = “Hello, Direct Value!”;
processAsyncValue(directValue);

すべてのケースで `processAsyncValue` は正しく動き、戻り値としてそれぞれの文字列型 (`Promise`) を返してくれます。この柔軟性こそが、`PromiseLike` を採用する最大のメリットです。

—

まとめ

いかがだったでしょうか?

  • `Promise` だけでなく、`.then()` を持つオブジェクト全般を許容したいときは `PromiseLike` を使う。
  • 引数には `PromiseLike | T` とすることで、直値すらもシームレスに受け入れられる高い汎用性を実現できる。
  • 複雑なチェーンを書く必要はなく、`await` にそのまま渡せばTypeScriptのコンパイラが完璧にハンドリングしてくれる。

非同期処理の型定義において、この `PromiseLike` の引き出しを持っておくと、外部ライブラリとの統合や、独自のユーティリティ関数を書く際に圧倒的な強みになります。

ぜひ、日々の開発の型設計に取り入れてみてくださいね。あなたのTypeScriptライフがより快適で堅牢なものになることを応援しています!

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