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

こんにちは!日々のTypeScriptのコーディング、お疲れ様です。
今日は、多くの開発者が「おっ」とつまずきやすい、でもここさえ押さえればグッとコードが洗練される『PromiseLike』という非常に強力な概念についてお話ししますね。

「非同期処理を扱う関数を作りたいけれど、同期的な値も一緒に受け取れるようにしたいな…」
現場でコードを書いていると、そんな場面に必ずと言っていいほど遭遇しますよね。

ここをクリアすれば、あなたの書くコードの柔軟性は一気に跳ね上がり、TypeScriptの型システムの本質をぐっとマスターできますよ。一緒に優しく紐解いていきましょう!

—

1. なぜ「Promise」だけでは不十分なのか?

まずは、私たちが普段よく使う `Promise` についておさらいしておきましょう。
TypeScriptで非同期処理の結果を受け取る関数を作る時、ついつい以下のような型定義をしてしまいがちです。

// Promiseしか受け取れない厳格な関数
function processUserData(userFetcher: () => Promise): void {
userFetcher().then(result => {
console.log(result);
});
}

このコード、一見すると何の問題もないように見えますよね。
でも、もしあなたのチームメンバーが、「すでに手元にある同期的な文字列」を返す関数を渡してきたらどうなるでしょうか?

// 同期的に文字列を返す関数
const getCachedUser = () => “Static User”;

// 怒られてしまう!
processUserData(getCachedUser);
// エラー: ‘() => string’ 型の引数は ‘() => Promise‘ 型のパラメータに割り当てられません。

TypeScriptのコンパイラは冷徹です。「`Promise`を返すって約束したのに、あんたの関数はただの`string`を返してるじゃないか!」とエラーを出してしまいます。

実務では、「APIから非同期で取る場合」もあれば「キャッシュから同期的にすぐ返す場合」もあります。呼び出し側からすれば「どっちだって同じ文字列を返すんだから受け取ってよ!」と思いますよね。これを解決するのが `PromiseLike` です。

—

2. 救世主 `PromiseLike` とは何か?

TypeScriptの標準ライブラリには、`PromiseLike` という型があらかじめ用意されています。

頭の中で、この型を次のようなイメージで捉えてみてください。

【PromiseLikeのイメージ図】
本物の Promise ──┐
├──> 「.then() さえ持っていれば、私は仲間として受け入れるよ!」
自作の Thenable ─┘ (TypeScriptの構造的型付けの真骨頂)

そう、`PromiseLike` は、厳密に `new Promise()` で作られたインスタンスである必要はありません。「`then` メソッドを持っているオブジェクト(一般に Thenable と呼ばれます)」であれば、すべて受け入れてくれる懐の深い型なんです。

基本的な使い方

では、先ほどの関数を `PromiseLike` を使って書き換えてみましょう。

// T または T を返す PromiseLike を受け取る型定義
type MaybePromise = T | PromiseLike;

function processUserData(userFetcher: () => MaybePromise): void {
// Promise.resolve() で包むことで、同期・非同期のどちらであっても安全に `.then()` や `await` が使える!
Promise.resolve(userFetcher()).then(result => {
console.log(`処理成功: ${result}`);
});
}

// 1. 本物の Promise を返す関数(OK!)
processUserData(async () => “Async User from API”);

// 2. ただの同期的な値を返す関数(OK!)
processUserData(() => “Sync User from Cache”);

素晴らしいですね! `Promise.resolve()` というJavaScript/TypeScriptの標準機能と組み合わせることで、「同期の値だろうが、非同期のPromiseだろうが、どちらもシームレスに同じ作法で扱える」という最高のDX(開発者体験)が手に入ります。

—

3. 陥りやすい罠と、コンパイラの裏側

ここで、初学者がよくやってしまう「惜しいミス」と、型システムが裏でどう動いているかを見ておきましょう。

罠:そのままプロパティにアクセスしようとする

`PromiseLike` はあくまで「`then` を持っているかもしれない(あるいは持っていないかもしれない)」ものです。そのため、受け取った直後に直接メソッドやプロパティを叩こうとすると、コンパイラに叱られます。

function handleData(data: PromiseLike) {
// NG:
// const doubled = data 2;
// エラー: ‘PromiseLike‘ 型で ” 演算子は使用できません。
}

正解のアプローチ:`await` か `Promise.resolve()` を使う

非同期関数(`async/await`)の文脈であれば、`await` を使うのが最もエレガントです。`await` は、本物の `Promise` だけでなく、`PromiseLike` も完璧にアンラップ(値を解決)してくれます。

async function handleDataSafely(data: PromiseLike): Promise {
// await を通すことで、PromiseLike が綺麗に number に剥がれる
const resolvedData = await data;
return resolvedData 2; // 安全に計算できる!
}

コンパイル時に、TypeScriptは「この値は非同期かもしれないし同期かもしれないけれど、`await`(あるいは `Promise.resolve`)のトランスフォームを通せば、確実に `number` としてランタイムで取り出せる」と推論し、型安全性を担保しているのです。

—

4. まとめ:一歩進んだエンジニアへ

今回は、関数型における `PromiseLike` を用いた柔軟な値の受け入れについて解説しました。

  • `Promise` だけに縛られるな:同期的な値やサードパーティ製の独自非同期オブジェクトも想定するなら `PromiseLike`(または `T | PromiseLike`)を使おう。
  • 構造的型付けの恩恵:`then` メソッドさえあれば、TypeScriptはそれを「非同期の仲間」として温かく迎え入れてくれる。
  • 取り扱いは `Promise.resolve()` や `await` で:包み込んでしまえば、同期・非同期の境界線を意識せずに安全に処理できる。

ここをクリアできれば、ライブラリの型定義を読んだり、より抽象度の高い汎用的な関数設計をしたりするときに、迷いが一気に消え去りますよ。

あなたのTypeScriptの引き出しが、また一つ強力なものになりましたね。
日々のコーディングを、ぜひ楽しんでいきましょう!

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