JavaScript Tips | 基本・共通ユーティリティ:型チェック – Promise 判定

JavaScript JavaScript
スポンサーリンク

Promise 判定とは何を見分けたいのか

ここでいう「Promise 判定」は、その値が「非同期処理を表す Promise なのかどうか」を見分けることです。
業務コードでは、async/await や API 呼び出し、DB アクセス、ファイル操作など、非同期処理が当たり前に出てきます。

そのときによくあるのが、「この関数は Promise を返すかもしれないし、同期値を返すかもしれない」というパターンです。
そういう関数の戻り値を扱うときに、「これは Promise なのか? それとも普通の値なのか?」を判定できると、コードをかなりきれいに書けます。


まずは「Promise とは何か」をざっくり整理する

Promise は、「今は結果がないけれど、将来どこかのタイミングで成功か失敗かが決まる値」です。
そして、「thencatch で結果を受け取れるオブジェクト」として扱われます。

const p = new Promise((resolve, reject) => {
  setTimeout(() => {
    resolve("完了");
  }, 1000);
});

p.then((result) => {
  console.log(result); // 1 秒後に「完了」
});
JavaScript

async 関数も、内部的には必ず Promise を返します。

async function fetchData() {
  return 42;
}

const result = fetchData(); // これは Promise
console.log(result instanceof Promise); // true
JavaScript

つまり、「非同期処理の結果を表す値=Promise」と考えておけば OK です。


instanceof Promise だけでは足りない理由

一見すると、「Promise 判定は value instanceof Promise でいいのでは?」と思うかもしれません。

const p = Promise.resolve(1);
console.log(p instanceof Promise); // true
JavaScript

これは確かに動きますが、実務では「Promise っぽいもの(thenable)」も扱う必要が出てきます。

例えば、自作の「Promise 風オブジェクト」や、外部ライブラリが返す「then メソッドだけ持っているオブジェクト」です。

const thenable = {
  then(onFulfilled) {
    onFulfilled(123);
  },
};
JavaScript

これは instanceof Promise では false ですが、await したり Promise.resolve に渡したりすると、Promise と同じように扱われます。

(async () => {
  const value = await thenable;
  console.log(value); // 123
})();
JavaScript

このように、「Promise かどうか」を広い意味で判定したいときは、instanceof Promise だけでは不十分です。


実務でよく使う「Promise っぽいかどうか」の判定

Promise や「Promise っぽいもの」をまとめて扱いたいとき、
よく使われるのが「thenable 判定」です。

「thenable」とは、「then メソッドを持っているオブジェクト」のことです。
Promise も thenable の一種です。

function isPromiseLike(value) {
  return (
    value !== null &&
    (typeof value === "object" || typeof value === "function") &&
    typeof value.then === "function"
  );
}
JavaScript

この関数は、「null ではなく」「オブジェクトか関数で」「then が関数として存在するもの」を true と判定します。

console.log(isPromiseLike(Promise.resolve(1))); // true
console.log(isPromiseLike({ then() {} }));      // true
console.log(isPromiseLike(123));                // false
console.log(isPromiseLike(null));               // false
console.log(isPromiseLike({}));                 // false
JavaScript

業務的には、「await できるものかどうか」「then でつなげるものかどうか」を知りたい場面が多いので、
isPromiseLike のような判定がかなり役に立ちます。


「純粋な Promise だけ」を判定したい場合

一方で、「自分たちが使っているのはネイティブの Promise だけで、thenable は対象外でいい」というケースもあります。
その場合は、instanceof Promise を使ったシンプルな判定で十分です。

function isPromise(value) {
  return value instanceof Promise;
}

console.log(isPromise(Promise.resolve(1))); // true
console.log(isPromise({ then() {} }));      // false
console.log(isPromise(123));                // false
JavaScript

どちらを使うかは、「自分のプロジェクトで thenable をどこまで許容するか」によります。
迷ったら、まずは「広めにとらえる isPromiseLike」を用意しておき、
「ネイティブ Promise だけに絞りたい場面」が出てきたら isPromise を追加する、という順番でもいいです。


同期値と Promise を一緒に扱うためのユーティリティ

Promise 判定が本当に効いてくるのは、「同期値かもしれないし Promise かもしれない」という戻り値を扱うときです。

例えば、次のような関数を考えます。

function maybeAsync(flag) {
  if (flag) {
    return Promise.resolve(42);
  }
  return 42;
}
JavaScript

この関数は、flag によって「同期値」か「Promise」かが変わります。
これを使う側で、毎回こう書くのは面倒です。

const result = maybeAsync(true);

if (isPromiseLike(result)) {
  result.then((v) => console.log(v));
} else {
  console.log(result);
}
JavaScript

そこで、「同期値でも Promise に包んでしまう」ユーティリティを用意すると、扱いが一気に楽になります。

function toPromise(value) {
  return isPromiseLike(value) ? value : Promise.resolve(value);
}

const p1 = toPromise(maybeAsync(true));
const p2 = toPromise(maybeAsync(false));

Promise.all([p1, p2]).then(([v1, v2]) => {
  console.log(v1, v2); // 42 42
});
JavaScript

ここで isPromiseLike を使っているおかげで、「すでに Promise ならそのまま」「そうでなければ Promise.resolve で包む」という共通処理が書けます。
実務では、この「同期・非同期を一旦 Promise にそろえる」テクニックがかなり強力です。


async/await と Promise 判定の関係

async 関数は、必ず Promise を返します。

async function f() {
  return 1;
}

console.log(isPromise(f())); // true
JavaScript

そのため、「この関数は async かどうか」を知りたいときも、
「戻り値が Promise かどうか」を見ればだいたい分かります。

ただし、普通の関数でも Promise を返すことはできるので、
「async かどうか」を厳密に判定するのはあまり意味がありません。

実務的には、「戻り値が Promise(または Promise っぽいもの)かどうか」を見て、
await すべきかどうか」「then でつなぐべきかどうか」を決める、という発想で十分です。


実務での具体的な利用イメージ

フック関数が同期でも非同期でも動くようにする

例えば、「前処理フック」を受け取るユーティリティを考えます。

async function runWithHook(hook, task) {
  const hookResult = hook?.();

  // hook が同期でも非同期でも待てるようにする
  if (isPromiseLike(hookResult)) {
    await hookResult;
  }

  return task();
}
JavaScript

ここでは、「hook が Promise を返すかもしれないし、何も返さないかもしれない」という前提で、
isPromiseLike を使って「必要なら await する」という書き方をしています。

これにより、呼び出し側は「同期フック」でも「非同期フック」でも自由に書けるようになります。

runWithHook(
  () => console.log("同期フック"),
  () => console.log("タスク")
);

runWithHook(
  async () => {
    console.log("非同期フック開始");
    await new Promise((r) => setTimeout(r, 1000));
    console.log("非同期フック終了");
  },
  () => console.log("タスク")
);
JavaScript

エラーハンドリングの統一

同期処理と非同期処理をまとめて扱いたいときにも、Promise 判定+ toPromise が役立ちます。

function runSafely(fn) {
  try {
    const result = fn();
    return toPromise(result).catch((err) => {
      console.error("エラー:", err);
    });
  } catch (err) {
    console.error("エラー:", err);
    return Promise.resolve();
  }
}
JavaScript

ここでは、「fn が同期エラーを投げる場合」と「Promise を返して非同期エラーになる場合」を両方カバーしています。
toPromise の中で isPromiseLike を使っているおかげで、「戻り値が何であっても最終的に Promise として扱える」ようになっています。


小さな練習で感覚をつかむ

最後に、手を動かして慣れるためのミニ課題を提案します。

自分で isPromise, isPromiseLike, toPromise を実装して、次の値を順番に渡してみてください。

Promise.resolve(1), new Promise(() => {}), { then() {} }, async () => {}, () => 1, 123, "abc", null, undefined

それぞれに対して、「isPromise はどう返すか」「isPromiseLike はどう返すか」「toPromise はどんな Promise を返すか」をコンソールに出してみると、
「Promise そのもの」と「Promise っぽいもの(thenable)」の違いが、かなりクリアに見えてきます。

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