← 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. If you annotate the return type explicitly, it must already be written as Promise<T> — async function f(): Promise<string> — because TypeScript treats async function f(): string as a compile error (TS1064: "the return type of an async function or method must be the global Promise<T> type"), not a shorthand for the same thing. Leaving the annotation off entirely is where the automatic wrapping shows up: TypeScript infers the Promise from the function's return statements on its own. 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 leaving the annotation off entirely produces the identical compiled behavior — TypeScript infers the Promise<User> wrapper automatically for any async function whose return type isn't annotated. What you can't do is annotate it as bare User; unlike a plain function, an async function's explicit return-type annotation must itself already be a Promise.

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.