TypeScriptにおける非同期プログラミングと型
非同期プログラミングはJavaScriptの中核となるパターンであり、TypeScriptでは、ジェネリックなPromiseやasync/awaitを通じて、非同期操作に対する完全な型サポートが追加されています。
1. Promise型の基礎
(1) プロミスの汎用型
Promise はジェネリック型であり、Promise<T> は「将来、型 T の値が生成される」ことを示しています:
TYPESCRIPT
// Synchronization Functions——Direct Return Value
function getUserSync(): string {
return "Charlie";
}
// Asynchronous Functions——Back Promise<string>
function getUserAsync(): Promise<string> {
return Promise.resolve("Charlie");
}
// Simulate Web Requests
function fetchUser(id: number): Promise<{ id: number; name: string }> {
return new Promise((resolve) => {
setTimeout(() => resolve({ id, name: "User" + id }), 100);
});
}
(2) then および catch における型推論
TYPESCRIPT
let promise: Promise<string> = Promise.resolve("hello");
promise
.then(value => {
// value Type automatically inferred as string
console.log(value.toUpperCase()); // ✅
return value.length; // Back number → then Chain becomes Promise<number>
})
.then(length => {
// length Type automatically inferred as number
console.log(length.toFixed(2)); // ✅
})
.catch(error => {
// error Type: any(TypeScript Unable to infer the error type)
console.log(error.message);
});
(3) Promise型の不変性と安全性
TYPESCRIPT
// Promise It is covariant.——Promise<string> Can be assigned to Promise<string | number>
let p1: Promise<string> = Promise.resolve("hello");
let p2: Promise<string | number> = p1; // ✅ Covariant Safety
// It doesn't work the other way around.
// let p3: Promise<string> = p2; // ❌ Promise<string | number> Cannot be assigned Promise<string>
▶ 例:型安全な API クライアント
TYPESCRIPT
interface ApiResponse<T> {
status: number;
data: T;
message: string;
}
interface User {
id: number;
name: string;
email: string;
}
interface Product {
id: number;
title: string;
price: number;
}
function apiGet<T>(url: string): Promise<ApiResponse<T>> {
// Simulation API Call
return new Promise((resolve) => {
setTimeout(() => {
resolve({
status: 200,
data: {} as T, // In actual projects, this is done by JSON.parse Result Assertion
message: "OK"
});
}, 100);
});
}
// Usage——Each request has a specific return type
async function getUser(id: number): Promise<User> {
let response = await apiGet<User>(`/api/users/${id}`);
return response.data;
}
async function getProducts(): Promise<Product[]> {
let response = await apiGet<Product[]>("/api/products");
return response.data;
}
2. async/await における型
(1) 非同期関数の戻り値
非同期関数は常に Promise を返します。通常の値を返した場合でも、TypeScript が自動的にそれをラップします:
TYPESCRIPT
// Explicit Return Values——Automatic packaging is Promise
async function greet(): Promise<string> {
return "Hello"; // Equivalent to return Promise.resolve("Hello")
}
// Explicit Return Promise——No double packaging
async function fetchName(): Promise<string> {
return Promise.resolve("Charlie"); // It won't turn into Promise<Promise<string>>
}
(2) await による型推論
await は Promise を展開します — await Promise<T> の型は T です:
TYPESCRIPT
async function example() {
let name: string = await Promise.resolve("Charlie"); // ✅ Unpack to string
let count: number = await Promise.resolve(42); // ✅ Unpack to number
let user: User = await fetchUser(1); // ✅ Unpack to User
}
(3) 落とし穴に注意 — 却下される可能性があります
TYPESCRIPT
async function riskyOperation(): Promise<number> {
// This Promise possibly reject
let result = await mayFail(); // result The type is number——However, an exception may be thrown during runtime.
return result;
}
async function mayFail(): Promise<number> {
if (Math.random() > 0.5) {
throw new Error("Random failure");
}
return 42;
}
// Safe coding practice — use try/catch
async function safeOperation(): Promise<number | null> {
try {
let result = await mayFail();
return result;
} catch {
return null;
}
}
3. Promise ユーティリティ型
(1) 待ち受け—Promise型の解明
TypeScript 4.5 には、Awaited<T> 型(Promise の再帰的なアンラップ)が導入されています:
TYPESCRIPT
type A = Awaited<Promise<string>>; // string
type B = Awaited<Promise<Promise<number>>>; // number(Recursive Unpacking)
type C = Awaited<string>; // string (non-Promise, returned directly)
type D = Awaited<Promise<string | number>>; // string | number
(2) 実用例—非同期関数の戻り値の型を取得する
TYPESCRIPT
async function fetchUser(id: number) {
let response = await fetch(`/api/users/${id}`);
return response.json() as Promise<{ id: number; name: string }>;
}
// Extracting the Return Type of an Asynchronous Function(Remove Promise Packaging)
type UserResponse = Awaited<ReturnType<typeof fetchUser>>;
// { id: number; name: string }
(3) カスタム・プロミス・ユーティリティ型
TYPESCRIPT
// Extract Promise Each element in the array Promise Parsed Type
type UnwrapPromiseArray<T> = {
[K in keyof T]: T[K] extends Promise<infer U> ? U : T[K];
};
type Input = [Promise<string>, Promise<number>, boolean];
type Output = UnwrapPromiseArray<Input>;
// [string, number, boolean]
4. 並行制御における型安全性
(1) Promise.all
TYPESCRIPT
async function loadDashboard() {
// Promise.all Accepting different types of Promise Array
let [users, products, stats] = await Promise.all([
fetchUsers(), // Promise<User[]>
fetchProducts(), // Promise<Product[]>
fetchStats() // Promise<Stats>
]);
// Each variable has its own type.
console.log(users.length); // User[].length
console.log(products[0].title); // Product.title
console.log(stats.totalUsers); // Stats.totalUsers
}
(2) Promise.allSettled
TYPESCRIPT
type SettledResult<T> = {
status: "fulfilled";
value: T;
} | {
status: "rejected";
reason: unknown;
};
async function tryMultipleApis() {
let results = await Promise.allSettled([
fetchFromApi1(), // Promise<User>
fetchFromApi2(), // Promise<User>
]);
for (let result of results) {
if (result.status === "fulfilled") {
console.log(result.value.name); // ✅ value Type: User
} else {
console.log(result.reason); // unknown
}
}
}
(3) Promise.race と Promise.any
TYPESCRIPT
// race——Take the result that was completed first(Whether success or failure)
async function fetchWithTimeout(url: string, ms: number) {
let result = await Promise.race([
fetch(url),
new Promise<never>((_, reject) =>
setTimeout(() => reject(new Error("Timeout")), ms)
)
]);
return result;
}
// any——Take the first successful result(Ignore Failure)
async function fetchWithFallback() {
let result = await Promise.any([
fetchFromPrimary(), // Priority
fetchFromSecondary(), // Standby
]);
// result Type: All Promise composite types' resolved values
}
▶ 例:並行データローダー
TYPESCRIPT
interface User { id: number; name: string; }
interface Post { id: number; title: string; authorId: number; }
interface Comment { id: number; postId: number; text: string; }
async function fetchAllData(userId: number) {
let [user, posts, comments] = await Promise.all([
fetchUser(userId),
fetchPosts(userId),
fetchComments(userId)
]);
return { user, posts, comments };
}
async function fetchUser(id: number): Promise<User> {
return { id, name: "User" + id };
}
async function fetchPosts(userId: number): Promise<Post[]> {
return [{ id: 1, title: "Article1", authorId: userId }];
}
async function fetchComments(userId: number): Promise<Comment[]> {
return [{ id: 1, postId: 1, text: "Great Article!" }];
}
async function main() {
let data = await fetchAllData(1);
console.log(`User:${data.user.name}`);
console.log(`Number of Articles:${data.posts.length}`);
console.log(`Number of comments:${data.comments.length}`);
}
main();
5. 非同期イテレータ
(1) for await...of を使用した AsyncIterable
TYPESCRIPT
async function* asyncCounter(max: number): AsyncGenerator<number> {
for (let i = 0; i < max; i++) {
await new Promise(resolve => setTimeout(resolve, 100));
yield i;
}
}
async function run() {
for await (let num of asyncCounter(3)) {
console.log(num);
}
// 0 → 1 → 2(Every interval100ms)
}
(2) 非同期ジェネレータの種類
TYPESCRIPT
interface AsyncGenerator<T> {
next(): Promise<IteratorResult<T>>;
return(value?: any): Promise<IteratorResult<T>>;
throw(e?: any): Promise<IteratorResult<T>>;
[Symbol.asyncIterator](): AsyncGenerator<T>;
}
❓ よくある質問
Q
async 関数は Promise を返さなければならないのでしょうか?A 明示的に指定することをお勧めします。TypeScript は
async 関数が Promise を返すことを推論できますが、明示的に指定することで async function fn(): Promise<T> より明確になります。これにより、ドキュメント作成が容易になり、コンパイル時にエラーを検出できるようになります。単純な関数では戻り値の型を省略することもできますが、複雑な関数では明示的に指定することを推奨します。Q Promiseのcatchブロックでエラーの型が「unknown」となるのはなぜですか?
A JavaScriptでは(Errorに限定されず)任意の値がスローされるため、TypeScriptはキャッチされた値がError型であることを保証できません。catchブロック内で型チェックを行うこと
if (error instanceof Error)、または型アサーションを使用すること error as Error が推奨されます。Q
Promise.all 内のプロミスのいずれかが失敗した場合はどうなりますか?A
Promise.all は「オール・オア・ナッシング」方式で動作します。つまり、1つでもプロミスが失敗すると、all 操作全体が失敗となります。「すべてが完了する(成功・失敗にかかわらず)」必要がある場合は、Promise.allSettledを使用してください。「最初に成功したもの」が必要な場合は、Promise.anyを使用してください。Q 非同期関数はオーバーロードできますか?
A はい。非同期関数のオーバーロードは、同期関数とまったく同じように機能します。オーバーロードされたシグネチャは、さまざまなパラメータの組み合わせ(Promiseラッパー)に対する戻り値の型を記述しており、実装によってすべてのオーバーロード間でシグネチャの互換性が確保されます。
📖 まとめ
Promise<T>は非同期操作の一種であり、T は将来生成される値型である- 非同期関数は常に Promise を返します。
awaitは Promise を展開して T を取得します。 Awaited<T>プロミス型の再帰的な展開――非同期関数の戻り値型の抽出- Promise.all は複数の Promise を並行して実行し、タプル(各要素の型が異なる)を返します。
- Promise.allSettled はすべてが完了するのを待ち、Promise.race は最も速いものを取得し、Promise.any は最初に成功したものを取得します
- 非同期ジェネレータ
AsyncGenerator<T>はfor await...ofと連携して、非同期反復処理を実現します
📝 練習問題
- 基本問題(難易度 ⭐):ms ミリ秒後に解決する Promise を返す
delay(ms: number): Promise<void>という名前の関数を記述してください。async/await を使用してこの関数を呼び出し、「1 秒待ってからメッセージを出力する」という処理を実装してください。 - 上級問題(難易度 ⭐⭐):
retry<T>(fn: () => Promise<T>, maxRetries: number): Promise<T>関数を作成してください。fn が失敗した場合、最大 maxRetries 回まで自動的に再試行します。try/catch とループを使用してこれを実装してください。 - チャレンジ問題(難易度 ⭐⭐⭐):
concurrentLimit<T>(tasks: (() => Promise<T>)[], limit: number): Promise<T[]>を実装してください。タスクを並行して実行しますが、同時に実行するタスクの数は上限を超えないようにしてください。すべてのタスクが完了したら、結果の配列(同じ順序で)を返してください。



