こんにちは!日々のTypeScriptのコーディング、お疲れ様です。
今日は、多くの開発者が「おっ」とつまずきやすい、でもここさえ押さえればグッとコードが洗練される『PromiseLike』という非常に強力な概念についてお話ししますね。
「非同期処理を扱う関数を作りたいけれど、同期的な値も一緒に受け取れるようにしたいな…」
現場でコードを書いていると、そんな場面に必ずと言っていいほど遭遇しますよね。
ここをクリアすれば、あなたの書くコードの柔軟性は一気に跳ね上がり、TypeScriptの型システムの本質をぐっとマスターできますよ。一緒に優しく紐解いていきましょう!
—
1. なぜ「Promise」だけでは不十分なのか?
まずは、私たちが普段よく使う `Promise` についておさらいしておきましょう。
TypeScriptで非同期処理の結果を受け取る関数を作る時、ついつい以下のような型定義をしてしまいがちです。
// Promiseしか受け取れない厳格な関数
function processUserData(userFetcher: () => Promise
userFetcher().then(result => {
console.log(result);
});
}
このコード、一見すると何の問題もないように見えますよね。
でも、もしあなたのチームメンバーが、「すでに手元にある同期的な文字列」を返す関数を渡してきたらどうなるでしょうか?
// 同期的に文字列を返す関数
const getCachedUser = () => “Static User”;
// 怒られてしまう!
processUserData(getCachedUser);
// エラー: ‘() => string’ 型の引数は ‘() => Promise
TypeScriptのコンパイラは冷徹です。「`Promise
実務では、「APIから非同期で取る場合」もあれば「キャッシュから同期的にすぐ返す場合」もあります。呼び出し側からすれば「どっちだって同じ文字列を返すんだから受け取ってよ!」と思いますよね。これを解決するのが `PromiseLike` です。
—
2. 救世主 `PromiseLike` とは何か?
TypeScriptの標準ライブラリには、`PromiseLike
頭の中で、この型を次のようなイメージで捉えてみてください。
【PromiseLikeのイメージ図】
本物の Promise ──┐
├──> 「.then() さえ持っていれば、私は仲間として受け入れるよ!」
自作の Thenable ─┘ (TypeScriptの構造的型付けの真骨頂)
そう、`PromiseLike` は、厳密に `new Promise()` で作られたインスタンスである必要はありません。「`then` メソッドを持っているオブジェクト(一般に Thenable と呼ばれます)」であれば、すべて受け入れてくれる懐の深い型なんです。
基本的な使い方
では、先ほどの関数を `PromiseLike` を使って書き換えてみましょう。
// T または T を返す PromiseLike を受け取る型定義
type MaybePromise
function processUserData(userFetcher: () => MaybePromise
// 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
// await を通すことで、PromiseLike
const resolvedData = await data;
return resolvedData 2; // 安全に計算できる!
}
コンパイル時に、TypeScriptは「この値は非同期かもしれないし同期かもしれないけれど、`await`(あるいは `Promise.resolve`)のトランスフォームを通せば、確実に `number` としてランタイムで取り出せる」と推論し、型安全性を担保しているのです。
—
4. まとめ:一歩進んだエンジニアへ
今回は、関数型における `PromiseLike` を用いた柔軟な値の受け入れについて解説しました。
- `Promise` だけに縛られるな:同期的な値やサードパーティ製の独自非同期オブジェクトも想定するなら `PromiseLike
`(または `T | PromiseLike `)を使おう。 - 構造的型付けの恩恵:`then` メソッドさえあれば、TypeScriptはそれを「非同期の仲間」として温かく迎え入れてくれる。
- 取り扱いは `Promise.resolve()` や `await` で:包み込んでしまえば、同期・非同期の境界線を意識せずに安全に処理できる。
ここをクリアできれば、ライブラリの型定義を読んだり、より抽象度の高い汎用的な関数設計をしたりするときに、迷いが一気に消え去りますよ。
あなたのTypeScriptの引き出しが、また一つ強力なものになりましたね。
日々のコーディングを、ぜひ楽しんでいきましょう!