thecodex.expert · The Codex Family of Knowledge
TypeScript

Typing Async Code

An async function that looks like it returns a User actually returns a Promise<User> — TypeScript adds that wrapper automatically, whether you write it explicitly or not.

TypeScript 6.0 Promise<T> Last verified:
Canonical Definition

Any function marked async automatically returns a Promise wrapping its stated or inferred return type — writing async function f(): Promise<string> is equivalent to writing async function f(): string, since TypeScript wraps it either way. Inside another async function, await unwraps that Promise back to its inner type, and TypeScript tracks this transformation precisely at every step, catching an accidental double-wrap or a missing await.

async functions always return a Promise

Writing the return type as Promise<User> or just User produces the identical compiled behavior — TypeScript infers the Promise wrapper automatically for any async function.

TypeScriptasync_basics.ts
interface User { name: string; }

async function fetchUser(): Promise<User> {
    const response = await fetch("/api/user");
    return response.json();   // TypeScript trusts json()'s declared type here
}

async function caller() {
    const user = await fetchUser();   // awaited: user is User, not Promise<User>
    console.log(user.name);
}

Typing Promise.all and concurrent operations

Promise.all's return type is inferred as a tuple matching each input promise's resolved type — TypeScript tracks each position individually, even when the promises resolve to different types.

TypeScriptpromise_all.ts
async function fetchUser(): Promise<{ name: string }> { /* ... */ return { name: "Alice" }; }
async function fetchPosts(): Promise<string[]> { /* ... */ return ["post1"]; }

async function loadDashboard() {
    const [user, posts] = await Promise.all([fetchUser(), fetchPosts()]);
    // user: { name: string }, posts: string[] — each correctly typed, not "any"
}

Errors: catch variables are unknown, not any

Since TypeScript 4.4, a caught error defaults to unknown rather than any under strict settings — forcing an explicit type check before accessing any property on it, since JavaScript allows throwing literally any value, not just Error objects.

TypeScriptasync_errors.ts
async function loadData() {
    try {
        return await fetchUser();
    } catch (err) {
        // err is unknown — must narrow before use
        if (err instanceof Error) {
            console.error(err.message);
        }
        throw err;
    }
}

Sources

1
Microsoft. TypeScript Handbook, "Async/Await" and TSConfig "useUnknownInCatchVariables", typescriptlang.org/docs/handbook.