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

こんにちは! TypeScriptの世界へようこそ。

日々フロントエンドやNode.jsの開発をしていると、「同期処理(普通の処理)と非同期処理(Promiseを使う処理)のどちらが渡されても、柔軟に受け取ってスマートに処理したい」という場面に必ず遭遇します。

たとえば、イベントハンドラーやプラグイン機能を作る時、使う側が `() => 100`(同期)と書くかもしれないし、 `async () => fetch(…)`(非同期)と書くかもしれませんよね。

「同期も非同期も両方ウェルカムだよ!」という寛大な関数をTypeScriptで美しく型定義する秘密兵器――それが`PromiseLike`です。

ここをマスターすると、TypeScriptの型システムが持つ「構造的型付け(Duck Typing)」の真髄と、非同期処理の柔軟性がスッキリ理解できるようになります。「ここをクリアすれば、TypeScriptの基本はバッチリマスターできますよ!」という思いを込めて、優しく、そして本質から深掘りして解説していきますね。

—

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

まずは、よくある困りごとから見てみましょう。

関数(コールバック)を受け取って実行する簡単なライブラリ関数を作るとします。最初は「非同期処理を受け取るから」と、素直に `Promise` を使って型を書きますよね。

// ❌ ちょっと不便な型定義の例
type AsyncCallback = () => Promise;

function processTask(callback: AsyncCallback): void {
// …
}

// ————————————————–
// 使う側のコード
// ————————————————–

// ① async関数を渡す → OK!
processTask(async () => {
return “成功データ”; // 自動的に Promise になる
});

// ② 同期関数を渡す → エラー!
processTask(() => {
return “即座に返すデータ”; // 型エラー:Type ‘string’ is not assignable to type ‘Promise‘.
});

「ただ値を返したいだけなのに、わざわざ `async () => …` と書くか `Promise.resolve(…)` で囲まないといけないの?」と、使う側が少し使いづらく感じてしまいますよね。

さらに問題があります。JavaScriptの世界には、ES2015で標準化された本物の `Promise` オブジェクト以外にも、「Promiseっぽく振る舞うオブジェクト(Thenableオブジェクト)」 が存在します。他言語からの移植ライブラリや古い軽量ライブラリなどで独自の `.then()` を持っているオブジェクトがそれです。

標準の `Promise` 型を指定してしまうと、こうした「Promiseっぽいオブジェクト」も弾かれてしまうのです。

—

2. 救世主「`PromiseLike`」と「Thenable」の正体

ここで登場するのが、TypeScriptの組み込み型である `PromiseLike` です。

`Promise` と `PromiseLike` の違い

図解すると、以下のような関係性になっています。

【 PromiseLike 】 (インターフェース:.then メソッドを持っているだけ)
▲
│ 継承・内包
│
【 Promise 】 (クラス:.then に加えて .catch, .finally などをフル装備)

TypeScriptの型定義(`lib.es5.d.ts`)を覗いてみると、`PromiseLike` は非常にシンプルに定義されています。

// TypeScript標準ライブラリの中身(概念コード)
interface PromiseLike {
then(
onfulfilled?: ((value: T) => TResult1 | PromiseLike) | undefined | null,
onrejected?: ((reason: any) => TResult2 | PromiseLike) | undefined | null
): PromiseLike;
}

要するに、「詳細なクラスの実装はどうあれ、とにかく `.then()` というメソッドさえ持っていれば `PromiseLike` とみなすよ」 という契約なのです。TypeScriptは「構造的型付け(Duck Typing)」という仕組みを採用しているため、名前ではなく「どんなプロパティやメソッドを持っているか」で型を判定します。

  • `Promise`: 本物のPromiseインスタンス(`.catch()` や `.finally()` も必要)
  • `PromiseLike`: 「Thenable(`.then` を持つもの)」全般を受け入れる広い型

—

3. 同期・非同期をどちらも受け取る「最強の型パターン」

それでは、同期関数と非同期関数のどちらが渡されても完璧に受け止められる関数型を定義してみましょう!

実務で非常によく使われるテクニックが、「値そのもの(T)」と「Promiseっぽいもの(PromiseLike)」の和集合(Union型) を作ることです。

基本の型定義ヘルパー

// 「T または T を包んだ PromiseLike」を表すユーティリティ型
type Awaitable = T | PromiseLike;

この `Awaitable`(`MaybePromise` と呼ばれることもあります)を使うことで、関数の引数型を極限まで柔軟にできます。

—

4. 実践コード:同期・非同期を柔軟に飲み込む `runTask` 関数

具体的なコードで動かしてみましょう!
関数を受け取り、それが同期処理でも非同期処理でも正しく実行して結果を返す汎用関数を作成します。

// 1. 柔軟なコールバック関数の型を定義
// 戻り値が 「T」 でも 「PromiseLike」 でもOK!
type FlexibleTask = () => T | PromiseLike;

/

  • 同期・非同期問わず、タスクを実行して結果をPromiseで返す関数

/
async function runTask(task: FlexibleTask): Promise {
console.log(“— タスクを開始します —“);

// JavaScriptの `await` は超絶優秀!
// 値が Promise/PromiseLike なら完了を待ち、普通の「値」ならそのまま通過させます。
const result: T = await task();

console.log(“— タスクが完了しました —“);
return result;
}

// ————————————————–
// 実際に使ってみよう!
// ————————————————–

async function main() {
// パターン①:同期関数を渡す(ただの数値を返す)
const result1 = await runTask(() => {
return 42; // 普通の number を返しても型エラーにならない!
});
console.log(“実行結果1:”, result1); // 出力: 42

// パターン②:標準の async 関数(Promise)を渡す
const result2 = await runTask(async () => {
// 疑似的な非同期処理(100ms待つ)
await new Promise((resolve) => setTimeout(resolve, 100));
return “非同期成功!”;
});
console.log(“実行結果2:”, result2); // 出力: 非同期成功!

// パターン③:自作の Thenable オブジェクトを返す(PromiseLike)
const result3 = await runTask(() => {
// .then メソッドだけを持つオブジェクトを返す
return {
then(onfulfilled?: (value: string) => void) {
if (onfulfilled) onfulfilled(“Thenableからのデータ”);
},
};
});
console.log(“実行結果3:”, result3); // 出力: Thenableからのデータ
}

main();

実行結果のイメージ

— タスクを開始します —
— タスクが完了しました —
実行結果1: 42
— タスクを開始します —
— タスクが完了しました —
実行結果2: 非同期成功!
— タスクを開始します —
— タスクが完了しました —
実行結果3: Thenableからのデータ

どうでしょうか? `runTask` を呼び出す側は、関数の前に `async` を付けるかどうかを気にする必要がなくなりましたよね!

—

5. 陥りやすい罠とハマりポイント

ここで、初心者のエンジニアがよく遭遇する文法エラーや誤解について補足しておきますね。

罠①:関数の「戻り値」ではなく「引数」に PromiseLike を直接書いてしまう

// ❌ 間違いやすい例
function processValue(input: PromiseLike) {
// これだと「普通の生の値(例: number)」を直接渡せなくなってしまう!
}

processValue(100); // エラー! Argument of type ‘number’ is not assignable to parameter of type ‘PromiseLike‘.

解決策:
「生の生データも受け取りたい」なら、必ず `T | PromiseLike` という Union型(和集合) にする必要があります。

罠②:`await` を忘れて直接返してしまう

受け取った `task()` を実行する側で `await` を忘れると、戻り値の型がブレてしまいます。

// ❌ あまり良くない例(awaitがない)
function runTaskBad(task: FlexibleTask): Promise {
// task() の結果は T | PromiseLike なので、
// そのまま返すなら Promise.resolve で包む等の考慮が必要
return Promise.resolve(task());
}

TypeScriptにおいて、`async function` の中で `await` を使うと、コンパイラは `T | PromiseLike` を見事に「解凍(Unwrap)」して、純粋な `T` 型に絞り込んで(Narrowing)くれます。

`await` キーワードは、実行時(JavaScript)の動作だけでなく、コンパイル時(TypeScript)の型推論においても強力なアンラッパー(型を剥がす道具) として機能しているのです。

—

6. まとめ:柔軟な型定義で「優しいコード」を書こう

今回のポイントを整理してみましょう!

1. `Promise` は厳格(標準クラスのフル機能を要求する)
2. `PromiseLike` は柔軟(`.then()` さえあればOKとする構造的型付け)
3. `T | PromiseLike` を使うことで、同期・非同期のどちらでも受け取れる柔軟な関数型が作れる
4. `await` は型をアンラップしてくれるので、実装側は `await` ひとつで両方を統一的に扱える

コンパイラを納得させるためだけに無駄な `async` や `Promise.resolve()` を書かせるのは、コードの読みやすさを損なってしまいます。

今回学んだ `PromiseLike` や `T | PromiseLike` という型テクニックを使えば、「呼び出す側には自由に書いてもらい、ライブラリ側でスマートに受け止める」 という、プロフェッショナルで優しいインターフェースが設計できるようになります。

ここまでの型システムの挙動を理解できれば、TypeScriptの基本どころか、非同期型の応用ステップへ進む準備はバッチリマスターできていますよ!

ぜひ、ご自身のプロジェクトの関数定義でも試してみてくださいね。応援しています!

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