【入門編】非同期関数(async)の戻り値型とPromiseの型推論 – TypeScript コア・型システムの基礎解析バイブル

はい、承知いたしました。TypeScriptの型システムの奥深さに触れつつ、async関数の戻り値型とPromiseの型推論について、初学者の方々にも優しく、しかし本質を掴めるように解説するブログ記事を執筆しますね。

—

TypeScriptの非同期処理を極める!async関数の「真の」戻り値型とPromiseの型推論を徹底解剖

こんにちは!皆さんは普段、TypeScriptを使って非同期処理を書いていますか?現代のWeb開発やNode.js開発では、ネットワーク通信やファイルI/Oなど、非同期処理が欠かせない要素になっていますよね。その中で、`async`/`await` は非同期処理をまるで同期処理のように記述できる、非常に強力で便利な機能です。

しかし、「`async` 関数が返す型って、結局何なんだろう?」「`Promise` が絡むと、型定義が少し複雑に見えるな…」と感じたことはありませんか?特にTypeScriptを学び始めたばかりの方や、他の言語から来た方にとっては、この部分が少し戸惑うポイントになるかもしれません。

安心してください!この記事では、`async` 関数の型システムにおける振る舞いを、初学者の方にも分かりやすく、しかしTypeScriptの設計思想の核心に迫って解説していきます。ここをしっかり理解できれば、TypeScriptでの非同期処理の型定義はバッチリマスターできますよ!

🚀 まずは基本から!`async` 関数って何だっけ?

ご存知の通り、`async` キーワードを関数に付けると、その関数は非同期関数になります。そして、その中で `await` キーワードを使うことで、`Promise` の解決を待つことができますよね。

簡単な例を見てみましょう。

// 非同期に文字列を返す関数
async function sayHello(): Promise {
// 1秒待ってから文字列を返します
return new Promise((resolve) => {
setTimeout(() => {
resolve(“Hello, TypeScript!”);
}, 1000);
});
}

// sayHello関数を呼び出して結果を待つ
async function main() {
console.log(“処理を開始します…”);
const message = await sayHello(); // awaitでPromiseの解決を待つ
console.log(message); // “Hello, TypeScript!”
console.log(“処理を終了しました。”);
}

main();

上記のコードは、`sayHello` 関数が `Promise` を返し、`main` 関数でそれを `await` して、結果の文字列 `message` を受け取っています。これは皆さんにとってお馴染みの使い方だと思います。

さて、ここからが本題です。

🎭 async関数の「真の」戻り値型:Promiseのベールを剥がす

実は、`async` キーワードが付いた関数は、内部でどんな値を `return` しようと、必ず `Promise` 型を返します。 これが `async` 関数の型システムにおける最も重要なルールになります。

「え、でも `string` を返す関数だったら `string` を返すんじゃないの?」って思いますよね。いいえ、TypeScriptの型評価は、そうはなりません。

例えば、次のような関数を考えてみましょう。

// async関数が「数値」を返すように見える例
async function getNumber(): number { // 🔴 ここに注目!戻り値型を number と指定しています
return 123; // 数値を直接返しています
}

このコード、実はコンパイルエラーになります。

Type ‘Promise‘ is not assignable to type ‘number’.
Type ‘Promise‘ is missing the following properties from type ‘number’: toFixed, toExponential, toPrecision, valueOf, and 2 others.

エラーメッセージが言っていますね。「`Promise` 型は `number` 型に割り当てられません」と。
これはまさに、`getNumber` 関数が `async` キーワードを持つために、たとえ `return 123;` と書かれていても、TypeScriptのコンパイラはこの関数の戻り値型を `Promise` と推論していることを示しています。

イメージ図で理解するasync関数の戻り値

async関数を、まるで「Promiseでラッピングする機械」だと考えてみてください。

┌────────────────┐
│ async 関数 │
│ (Promiseラッパー)│
└──────┬─────────┘
│
return 123 (number)
│
▼
┌────────────────┐
│ Promise │
│ (Promise)│
└────────────────┘

つまり、`async` 関数の中で `return` された値は、自動的に `Promise.resolve()` でラップされ、その `Promise` が関数の最終的な戻り値となる、という実行時の挙動と、TypeScriptの型システムが完全に同期しているわけです。

したがって、正しく型を定義するならこうなります。

// async関数は常にPromiseを返すので、戻り値型もPromiseでラップします
async function getNumber(): Promise {
return 123; // Promiseで自動的にラップされ、Promiseとして返されます
}

async function getString(): Promise {
return “Hello”; // Promiseとして返されます
}

async function getBoolean(): Promise {
return true; // Promiseとして返されます
}

// voidを返すasync関数は、Promiseを返します
async function doSomething(): Promise {
console.log(“何か処理を実行します…”);
// return文がない場合や、return; と書いた場合は Promise になります
}

// これらの関数を呼び出す時は、awaitするかthenを使う必要があります
async function testAsyncReturns() {
const num: number = await getNumber(); // awaitでPromiseを解決して number を取り出す
console.log(`取得した数値: ${num}`); // 123

const strPromise: Promise = getString(); // awaitしなければPromiseのまま
const str: string = await strPromise;
console.log(`取得した文字列: ${str}`); // Hello

await doSomething(); // 処理を実行
}

testAsyncReturns();

このように、`async` 関数を定義する際には、戻り値型を必ず `Promise` の形式で指定するようにしましょう。`T` には、`async` 関数内で `return` される値の型が入ります。

🤯 Promiseの型推論の深掘り:なぜ `Promise` にならないのか?

ここからが、TypeScriptの型システムの賢さに触れる部分です。
もし `async` 関数の中で、すでに `Promise` を返していたらどうなるでしょう?

async function getNestedPromise(): Promise> { // 🔴 ここはこう書くべき?
// 既にPromiseで解決された値を返しています
return Promise.resolve(“Nested Promise Value”);
}

直感的に考えると、`async` 関数が `Promise` を返すから、`Promise.resolve(“…”)` を返すと `Promise>` になるんじゃないか?と思いますよね。

しかし、TypeScriptのコンパイラは、こんな風に型推論してくれます。

async function getNestedPromise() {
return Promise.resolve(“Nested Promise Value”);
}

// getNestedPromise の推論される型は Promise です!
type InferredType = ReturnType;
// ^? type InferredType = Promise

なんと、`Promise>` にはならず、きちんと`Promise` と推論されていますね!これは一体なぜでしょうか?

TypeScriptの「Promise自動平坦化(flattening)」の知恵

TypeScriptのコンパイラは非常に賢く設計されており、`async` 関数が返す値が既に `Promise` である場合、それを二重に `Promise` でラップするようなことはしません。JavaScriptの `Promise` の仕様では、`Promise.resolve(somePromise)` のように、`Promise` を解決する値がさらに `Promise` であった場合、そのネストされた `Promise` が解決するのを待って、最終的な値を取り出す、という振る舞いをします。

このJavaScriptのランタイムの振る舞い(いわゆる「thenable」の解決)と完全に一致するように、TypeScriptの型システムも設計されています。つまり、`async` 関数の型推論は、最終的に `Promise` が解決して得られる値の型に合わせようとします。

async function fn() {
return Promise.resolve(Promise.resolve(“hello”));
}
// この fn の戻り値型は Promise になります
// Promise>> とはなりません。

これは、開発者が直感的に非同期処理の型を扱えるようにするための、TypeScriptの強力な「型の正規化」プロセスの一つなんです。コンパイラは、ネストされた `Promise` の解決ステップを型レベルでシミュレートし、最終的な「剥き出しの」値の型を `Promise` のジェネリクス (``) に当てはめてくれるんですね。これぞまさに、言語の重みを理解した上での設計思想が光る部分です。

✍️ 実践!async関数の戻り値型を明示的に定義する

型推論は非常に便利ですが、明示的に型を定義することには大きなメリットがあります。

1. 可読性の向上: コードを読む人が、この関数が何を返すのか一目で理解できます。
2. 意図の明確化: 開発者の意図が明確になり、誤った型が返されることを防ぎます。
3. 堅牢性の向上: 将来的に関数の実装が変わった場合でも、型定義がガードレールとなってくれます。

例えば、ユーザーデータを非同期で取得する関数を考えてみましょう。

// ユーザーデータの型定義
interface User {
id: number;
name: string;
email: string;
}

// ユーザーデータを非同期で取得する関数
async function fetchUser(id: number): Promise {
// 実際のAPI呼び出しの代わりにダミーデータを返します
return new Promise((resolve) => {
setTimeout(() => {
if (id === 1) {
resolve({ id: 1, name: “Alice”, email: “alice@example.com” });
} else {
resolve(undefined); // ユーザーが見つからない場合は undefined
}
}, 500);
});
}

async function displayUser(userId: number) {
console.log(`ユーザーID ${userId} の情報を取得中…`);
const user = await fetchUser(userId); // await することで Promise から User | undefined を取り出す

if (user) {
console.log(`名前: ${user.name}, メール: ${user.email}`);
} else {
console.log(`ユーザーID ${userId} は見つかりませんでした。`);
}
}

displayUser(1); // 存在しないユーザー
displayUser(99); // 存在するユーザー

`fetchUser` 関数の戻り値型を `Promise` と明示することで、「この関数は `User` オブジェクトか、あるいは `undefined` を解決する `Promise` を返すんだな」ということが一目瞭然になりますよね。

🚨 陥りやすいエラーとその対策

`async` 関数の型を扱う上で、よくある間違いとその対処法を見ていきましょう。

エラー1: `async` 関数なのに `Promise` でラップしない戻り値型を指定してしまう

これは先ほども触れましたが、改めて確認です。

async function calculateSum(a: number, b: number): number { // 🔴 エラー!
return a + b;
}
// エラー: Type ‘Promise‘ is not assignable to type ‘number’.

対策: `async` 関数は必ず `Promise` を返します。そのため、戻り値型も `Promise` の形式で指定する必要があります。

async function calculateSum(a: number, b: number): Promise { // ✅ OK
return a + b;
}

エラー2: `async` 関数を呼び出した結果を `await` せずに、直接「中身」の型として扱おうとする

async function fetchConfig(): Promise<{ theme: string; version: number }> {
return { theme: “dark”, version: 1.0 };
}

// …別の場所で
const config: { theme: string; version: number } = fetchConfig(); // 🔴 エラー!
// エラー: Type ‘Promise<{ theme: string; version: number; }>‘ is not assignable to type ‘{ theme: string; version: number; }’.

`fetchConfig()` を呼び出すと、それは `Promise<{ theme: string; version: number }>` 型の値を返します。`await` せずに直接代入しようとすると、`Promise` オブジェクト自体を期待する型に代入しようとしているため、型エラーになります。

対策: `async` 関数の結果の「中身」を使いたい場合は、必ず `await` するか、`.then()` メソッドを使って `Promise` を解決してください。

async function processConfig() {
const config: { theme: string; version: number } = await fetchConfig(); // ✅ OK
console.log(`テーマ: ${config.theme}, バージョン: ${config.version}`);
}

processConfig();

✨ 応用:ユーティリティ型 `Awaited` の活用

非同期処理の結果、つまり `Promise` が解決した後の「中身」の型を抽出したい場面って、結構ありますよね。そんな時に非常に便利なのが、TypeScript 4.5 で導入されたユーティリティ型 `Awaited` です。

`Awaited` は、`Promise` 型や `thenable` 型から、その解決される値の型を抽出してくれます。

type MyPromiseType = Promise;
type MyStringType = Awaited;
// ^? type MyStringType = string

type AnotherPromiseType = Promise;
type AnotherArrayType = Awaited;
// ^? type AnotherArrayType = number[]

// Promiseでない型に適用しても、そのままの型が返されます
type NotAPromise = string;
type StillAString = Awaited;
// ^? type StillAString = string

// ネストされたPromiseにも対応
type NestedPromise = Promise>;
type UnwrappedBoolean = Awaited;
// ^? type UnwrappedBoolean = boolean

これはとても強力ですよね!特に、既存の `async` 関数の戻り値型から、`await` した後の型を安全に取得したい場合に重宝します。

例えば、先ほどの `fetchUser` 関数の `await` 後の型を取得したい場合。

interface User {
id: number;
name: string;
email: string;
}

async function fetchUser(id: number): Promise {
return new Promise((resolve) => { / … / });
}

// fetchUser 関数の戻り値型は Promise です
type FetchUserReturnType = ReturnType;
// ^? type FetchUserReturnType = Promise

// await 後の型、つまり Promise が解決された User | undefined 型を取得したい場合
type UserOrUndefined = Awaited;
// ^? type UserOrUndefined = User | undefined

// この型を使って変数宣言できます
const userResult: UserOrUndefined = { id: 2, name: “Bob”, email: “bob@example.com” };
// これは await fetchUser(someId) の結果と全く同じ型になります!

`Awaited` は、TypeScriptのコンパイラが内部で `await` キーワードの型推論をどのように行っているか、そのロジックを開発者向けに公開してくれているようなものなんです。これを使うことで、より型安全で意図が明確なコードが書けるようになりますね。

✅ まとめ:async関数とPromiseの型を完全に掌握!

お疲れ様でした!今回は `async` 関数の戻り値型と `Promise` の型推論について、かなり深く掘り下げて解説しました。

重要なポイントをもう一度おさらいしましょう。

  • `async` 関数は、その内部でどんな値を `return` しようと、常に `Promise` 型を返します。 `T` は `async` 関数が解決する値の型です。
  • TypeScriptのコンパイラは非常に賢く、`async` 関数が `Promise` を返す `Promise` を `return` した場合でも、自動的に「平坦化」して `Promise` 型に推論してくれます。これはJavaScriptの `Promise` の実行時動作と完全に一致するように設計されています。
  • 関数の戻り値型は、型推論に任せるだけでなく、`Promise` の形で明示的に定義することで、コードの可読性と堅牢性が向上します。
  • `async` 関数の結果の「中身」の型、つまり `Promise` が解決した後の型を取り出したい場合は、ユーティリティ型 `Awaited` が非常に便利です。

これで `async` 関数と `Promise` の型システムに関する基本的な理解から、TypeScriptの設計思想に裏打ちされた深い挙動まで、バッチリマスターできたのではないでしょうか。

TypeScriptでの非同期処理の型定義は、もう怖くありませんね!この知識を活かして、より堅牢で読みやすいTypeScriptコードを書いていきましょう!

—

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