20
5

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

TypeScriptのPromise.allは10個までしか処理できない

Last updated at Posted at 2022-04-29

前提

本記事の内容はTypeScript v4.5以降では改善されています。
動作確認はTypeScript v4.4.4で行っています。

Promise.allでビルドエラーが起こる

Promise.all()は配列など反復可能なオブジェクトを引数にとり、複数のPromiseを同時に実行するメソッドです。複数のAPIを一度にコールして、データを取得する際などによく使われていると思います。

async function fetch1 (): Promise<Data1> {
  // データを取得する処理
}

async function fetch2 (): Promise<Data2> {
  // データを取得する処理
}

async function fetch3 (): Promise<Data3> {
  // データを取得する処理
}

const [data1, data2, data3] = await Promise.all([
  fetch1(),
  fetch2(),
  fetch3()
])
// data1はData1型
// data2はData2型
// data3はData3型
// に推論される

このようにPromiseの解決された結果を配列1で受け取ることができます。

さて、ここで先日、新しくAPIを取得する処理を追加しようとしました。いつも通り既存のPromise.allの引数の配列に新しい非同期関数を追加し、その結果を受け取る配列の要素も一つ追加しました。

async function fetch1 (): Promise<Data1> {
  // データを取得する処理
}

...

async function fetch10 (): Promise<Data10> {
  // データを取得する処理
}

+ // 新たなデータ取得処理を追加
+ async function fetch11 (): Promise<Data11> {
+  // データを取得する処理
+ }

const [
  data1, data2, data3, data4, data5,
  data6, data7, data8, data9, daya10,
+ data11 // 結果を受け取る変数を追加
] = await Promise.all([
  fetch1(),
  fetch2(),
  fetch3(),
  fetch4(),  
  fetch5(),
  fetch6(),
  fetch7(),
  fetch8(),
  fetch9(),
  fetch10(),
+ fetch11(), // 引数の要素を追加
])

しかしこれを実行しようとするとビルドエラーが起こりました。

Tuple type '[unknown, unknown, unknown, unknown, unknown, unknown, unknown, unknown, unknown, unknown]' of length '10' has no element at index '10'.

Promise.allの引数に関数を追加して、受け取る変数側を追加し忘れることはよくあると思います。最初はそれが原因かと思って配列の要素の数を何度も確認しましたが、一致しています。

原因

配列の順番を変えるなど色々と試した結果、配列の要素数が10個のときはうまく動作するのに対し、11個になるとエラーになることが分かりました。

実際にTypeScriptのPromise.allの型定義を見てみると

// 要素数2のとき
all<T1, T2>(values: readonly [T1 | PromiseLike<T1>, T2 | PromiseLike<T2>]): Promise<[T1, T2]>;

// 要素数3のとき
all<T1, T2, T3>(values: readonly [T1 | PromiseLike<T1>, T2 | PromiseLike<T2>, T3 | PromiseLike<T3>]): Promise<[T1, T2, T3]>;

...

// 要素数10のとき
all<T1, T2, T3, T4, T5, T6, T7, T8, T9, T10>(values: readonly [T1 | PromiseLike<T1>, T2 | PromiseLike<T2>, T3 | PromiseLike<T3>, T4 | PromiseLike<T4>, T5 | PromiseLike<T5>, T6 | PromiseLike<T6>, T7 | PromiseLike<T7>, T8 | PromiseLike<T8>, T9 | PromiseLike<T9>, T10 | PromiseLike<T10>]): Promise<[T1, T2, T3, T4, T5, T6, T7, T8, T9, T10]>;

このように要素数が2から10の場合まで一つずつ定義されていました(マジか、、、と思いました)
この型定義は要素数10までしか書かれていないため、当然ながらPromise.all()の引数に10よりも大きい要素数を持つ配列を渡すと、エラーになっていたということです。

厳密にはPromiseが返す型(Promise<T>T)が全て同じ場合は、Promise.allの引数の配列の長さに制限はありません。(以下で型推論されるためです。)

all<T>(values: readonly (T | PromiseLike<T>)[]): Promise<T[]>;

解決策その1. ネストする

すでにサービスが稼働しているなど気軽にTypeScriptのバージョンを上げられない場合はこちらが良いかと思います。
ややハック的で、可読性は落ちる気がしますが、以下のようにネストすることで10個以上の処理をPromise.allで処理できます。
参考

const [
  [data1, data2, data3, data4, data5,data6, data7, data8, data9, daya10],
  [data11, data12, data13, data14, data15, data16, data17],  // 受け取る側も構造を合わせる
] = await Promise.all([
  Promise.all([
    fetch1(),
    fetch2(),
    fetch3(),
    fetch4(),  
    fetch5(),
    fetch6(),
    fetch7(),
    fetch8(),
    fetch9(),
    fetch10(),
  ]),
  Promise.all([ // 一つのPromise.allにつき10個以下になるようにネストする
    fetch11(),
    fetch12(),
    fetch13(),
    fetch14(),
    fetch15(),
    fetch16(),
    fetch17(),
  ])
])

ちなみに三重のネストをしても動作したので、理論的には上限なくPromise.allで同時に実行できると思います。(マシンリソース的にどうかはまた別の話ですが)

解決策その2. バージョンを上げる

バージョンを上げられる場合はv4.5以上に上げることで解決できます。
この問題はissueがこれまでに挙がっており、TypeScript v4.5から型定義が改善されています。
TypeScript v4.5リリースアナウンス

なにが起こっているのか簡単に見てみます。(詳しくはこちらの神記事をご覧ください。)
まず、TypeScript v4.5ではAwaited というユーティリティ型が追加されました。詳しい説明はここでは省きますが、以下のリリースアナウンスからの引用のように、Promiseをawaitした結果の型を取得できます。

// A = string
type A = Awaited<Promise<string>>;

// B = number
type B = Awaited<Promise<Promise<number>>>;

// C = boolean | number
type C = Awaited<boolean | Promise<number>>;

また、v4.5-betaでのPromise.allの型定義を引用します

all<T extends readonly unknown[] | []>(values: T): Promise<{ -readonly [P in keyof T]: Awaited<T[P]> }>;

引数valuesの型Tは[Promise<string>, Promise<number>, Promise<boolean>]のような配列(厳密にはタプル)になっています。[P in keyof T]の部分ではTのkey、つまりインデックス番号がPに入ります。Awaited<T[P]>では、T[P] = [Promise<string>, Promise<number>, Promise<boolean>]の中のP番目の要素となり、Awaited<Promise<string>>のようになります。上述したようにAwaited<Promise<string>>はPromiseをawaitした結果、つまりstringを返します。結果的に返り値の型はPromise<{[0]: string, [1]: number, [2]: boolean}>のようになり、これは引数valuesの配列の要素一つ一つのPromiseをawaitした結果の配列になっているといった流れです。

最後に

TypeScriptの型ってパズルみたいで面白いなと思いました。
間違いなどあればコメントでご指摘ください。
ここまで読んでいただきありがとうございました。

  1. 厳密には配列ではなくタプルとするのが正確かと思います。ざっくりと配列とは同じ型の要素を並べたもの、タプルは異なる型の要素を並べたものになります。
    この2つを明確に区別している人はあまり多くない気がするので、本記事では配列とタプルをほぼ同じ意味で使っています。

20
5
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
20
5

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?