Back to Blog
Lesson 12 of the TypeScript: Typing JavaScript with Confidence course
TypeScriptJuly 30, 20264 min read

Asynchronous Functions and Promises: A TypeScript Guide

Learn to type async functions and Promises in TypeScript. Master API response handling with generic types to ensure your network layer remains type-safe.

TypeScriptAsyncPromisesAPIWeb Development
Detailed view of colorful programming code on a computer screen.

Previously in this course, we covered defining function return types. While that taught us how to handle synchronous data, modern web development relies heavily on non-blocking operations. In this lesson, we'll extend those skills to master async functions and Promises.

Understanding the Promise Wrapper

In JavaScript, an async function always returns a Promise. A Promise is an object representing the eventual completion (or failure) of an asynchronous operation. In TypeScript, we represent this using the generic Promise<T> type, where T is the type of the value the promise eventually resolves to.

If you have a function that fetches a user's name as a string, its return type isn't just string—it’s Promise<string>.

Annotating Async Functions

When you mark a function with the async keyword, TypeScript requires the return type to be wrapped in a Promise. If you omit the explicit return type, TypeScript’s inference engine will usually calculate it for you, but explicit annotations are a best practice for API clients where you want to enforce a contract.

Consider this example of a basic task fetcher:

TYPESCRIPT
interface Task {
  id: number;
  title: string;
  completed: boolean;
}

// Correctly typing the async function return
async function fetchTask(id: number): Promise<Task> {
  const response = await fetch(CE9178">`/api/tasks/${id}`);
  const data: Task = await response.json();
  return data;
}

By explicitly setting : Promise<Task>, you guarantee that anyone calling fetchTask knows they must await the result or use .then().

Handling API Response Wrappers

Real-world APIs rarely return raw objects. They often wrap responses in metadata. Typing these wrappers is the first step in building a robust API client.

Imagine your task API returns a structure like this: { data: Task, status: number }. You should define an interface for the response wrapper and use it in your function signature.

TYPESCRIPT
interface ApiResponse<T> {
  data: T;
  status: number;
}

async function fetchTaskWrapped(id: number): Promise<ApiResponse<Task>> {
  const response = await fetch(CE9178">`/api/tasks/${id}`);
  // We cast or validate the response to match our wrapper
  return await response.json();
}

The <T> here is a generic type parameter. It allows us to reuse ApiResponse for different data shapes (like a list of tasks or a single user profile) while keeping the wrapper logic consistent.

Hands-on Exercise: Typing the Task Client

In our running project, let's create a function to fetch all tasks.

  1. Define an interface Task with id and title.
  2. Create an async function named getTasks that returns Promise<Task[]>.
  3. Inside, use a mock fetch (or Promise.resolve) to return an array of tasks.
TYPESCRIPT
// Your code here
async function getTasks(): Promise<Task[]> {
  // Simulate API call
  return Promise.resolve([{ id: 1, title: "Finish course" }]);
}

Common Pitfalls

  • Forgetting the Promise wrapper: A common mistake is annotating an async function as returning Task instead of Promise<Task>. TypeScript will throw an error because the function actually returns a promise object.
  • Implicit any in response.json(): The .json() method in the Fetch API returns any by default. Always assign the result to a typed variable or cast it to your interface to ensure type safety.
  • Ignoring Rejection: Remember that async functions can throw. While we are focusing on return types, keep in mind that your call site needs to handle potential errors, as discussed in Node.js Async Error Handling in Express: A Practical Guide.

FAQ

Why does my async function return Promise<void>? If your function does not explicitly return a value (e.g., it just performs a console.log or a side-effecting API call), TypeScript correctly infers or expects Promise<void>.

Can I use async/await without Promises? No. async functions are syntactic sugar for Promises. Every time you mark a function as async, you are opting into the Promise-based architecture.

How do I handle errors in these functions? You should return the expected type on success and throw an error on failure. The caller is responsible for using try/catch blocks to handle the rejection.

Recap

We've learned that async functions are typed by wrapping their resolution value in a Promise<T>. By using generic interfaces like ApiResponse<T>, we can maintain strict type safety even when APIs return complex, wrapped data structures. This is a critical foundation for the robust API client we are building throughout this course.

Up next: We will explore how to use Union Types for Flexibility to handle different task statuses in our client.

Similar Posts