Lesson 3.4: Typing Async Functions
Duration: 50 minutes
Learning Objectives
By the end of this lesson, you will be able to:
- Properly type async functions with Promise return types
- Use type inference effectively with async/await
- Type complex async patterns including generics
- Handle
unknownerror types in catch blocks - Create reusable typed async utilities
Introduction
TypeScript and async/await work beautifully together. The type system understands that async functions return Promises, and await automatically unwraps the Promise type. This lesson covers how to leverage TypeScript's type system to write safer asynchronous code.
Basic Async Function Types
Every async function returns a Promise. TypeScript wraps your return type in Promise<T> automatically.
// Explicit return type
async function fetchNumber(): Promise<number> {
return 42;
}
// TypeScript infers Promise<number>
async function fetchNumberInferred() {
return 42;
}
// Both functions have the same type: () => Promise<number>
The await Operator and Types
When you await a Promise, TypeScript gives you the resolved type:
async function example(): Promise<void> {
const promise: Promise<string> = fetchName();
const name: string = await promise; // await unwraps Promise<string> to string
console.log(name);
}
Return Type Best Practices
Always explicitly annotate return types for public functions:
// Good - explicit return type
async function getUser(id: number): Promise<User> {
const response = await fetch(`/api/users/${id}`);
return response.json();
}
// Less ideal - inferred type
async function getUserInferred(id: number) {
const response = await fetch(`/api/users/${id}`);
return response.json(); // Returns Promise<any>
}
The inferred version returns Promise<any> because response.json() returns any. Explicit types catch this problem.
Typing Async Function Parameters
Function parameters work the same as in synchronous functions:
interface FetchOptions {
timeout?: number;
retries?: number;
headers?: Record<string, string>;
}
async function fetchData(url: string, options: FetchOptions = {}): Promise<unknown> {
const { timeout = 5000, retries = 3 } = options;
// Implementation
}
Callback Parameters
When passing async functions as callbacks, type them properly:
// Type for an async callback
type AsyncCallback<T, R> = (item: T) => Promise<R>;
async function mapAsync<T, R>(items: T[], callback: AsyncCallback<T, R>): Promise<R[]> {
return Promise.all(items.map(callback));
}
// Usage
const users = await mapAsync([1, 2, 3], async (id) => fetchUser(id));
Generic Async Functions
Generics make async utilities flexible and type-safe.
Basic Generic Async Function
async function fetchAndParse<T>(url: string): Promise<T> {
const response = await fetch(url);
const data: T = await response.json();
return data;
}
// Usage - specify the expected type
interface User {
id: number;
name: string;
}
const user = await fetchAndParse<User>('/api/users/1');
// user is typed as User
Generic Wrapper Functions
async function withRetry<T>(operation: () => Promise<T>, maxRetries: number = 3): Promise<T> {
let lastError: Error | undefined;
for (let attempt = 1; attempt <= maxRetries; attempt++) {
try {
return await operation();
} catch (error) {
lastError = error instanceof Error ? error : new Error(String(error));
if (attempt < maxRetries) {
await delay(1000 * attempt);
}
}
}
throw lastError;
}
// Usage - T is inferred from the operation
const user = await withRetry(() => fetchUser(1));
// user is typed as User
Generic Result Types
interface AsyncResult<T> {
data: T | null;
error: Error | null;
loading: boolean;
}
async function safeAsync<T>(operation: () => Promise<T>): Promise<AsyncResult<T>> {
try {
const data = await operation();
return { data, error: null, loading: false };
} catch (error) {
return {
data: null,
error: error instanceof Error ? error : new Error(String(error)),
loading: false,
};
}
}
// Usage
const result = await safeAsync(() => fetchUser(1));
if (result.error) {
console.error(result.error.message);
} else {
console.log(result.data?.name); // data is User | null
}
Typing Error Handling
In TypeScript strict mode, caught errors are typed as unknown. You must narrow the type before using error properties.
Narrowing Error Types
async function handleRequest(): Promise<void> {
try {
await riskyOperation();
} catch (error: unknown) {
// Cannot access error.message directly - error is unknown
if (error instanceof Error) {
// Now TypeScript knows it's an Error
console.error('Message:', error.message);
console.error('Stack:', error.stack);
} else {
console.error('Unknown error:', error);
}
}
}
Type Guards for Custom Errors
interface ApiError {
code: string;
message: string;
statusCode: number;
}
function isApiError(error: unknown): error is ApiError {
return (
typeof error === 'object' &&
error !== null &&
'code' in error &&
'message' in error &&
'statusCode' in error &&
typeof (error as ApiError).code === 'string' &&
typeof (error as ApiError).message === 'string' &&
typeof (error as ApiError).statusCode === 'number'
);
}
async function callApi<T>(url: string): Promise<T> {
try {
const response = await fetch(url);
if (!response.ok) {
const errorBody = await response.json();
throw errorBody;
}
return response.json();
} catch (error: unknown) {
if (isApiError(error)) {
// Full type safety here
console.error(`API Error ${error.code}: ${error.message}`);
if (error.statusCode === 401) {
// Handle unauthorized
}
}
throw error;
}
}
Creating a Typed Error Handler
type ErrorHandler = (error: unknown) => void;
function createErrorHandler(handlers: {
onApiError?: (error: ApiError) => void;
onNetworkError?: (error: TypeError) => void;
onUnknownError?: (error: unknown) => void;
}): ErrorHandler {
return (error: unknown) => {
if (isApiError(error) && handlers.onApiError) {
handlers.onApiError(error);
} else if (error instanceof TypeError && handlers.onNetworkError) {
handlers.onNetworkError(error);
} else if (handlers.onUnknownError) {
handlers.onUnknownError(error);
}
};
}
// Usage
const handleError = createErrorHandler({
onApiError: (e) => console.log(`API: ${e.code}`),
onNetworkError: (e) => console.log(`Network: ${e.message}`),
onUnknownError: (e) => console.log('Unknown:', e),
});
Typing Promise Utilities
Typed Promise.all
TypeScript infers tuple types for Promise.all:
async function loadDashboard(): Promise<[User, Post[], Notification[]]> {
const result = await Promise.all([
fetchUser(1), // Promise<User>
fetchPosts(), // Promise<Post[]>
fetchNotifications(), // Promise<Notification[]>
]);
// result is [User, Post[], Notification[]]
const [user, posts, notifications] = result;
return [user, posts, notifications];
}
Typed Promise.allSettled
async function fetchMultipleUsers(ids: number[]): Promise<User[]> {
const results: PromiseSettledResult<User>[] = await Promise.allSettled(
ids.map((id) => fetchUser(id))
);
return results
.filter((r): r is PromiseFulfilledResult<User> => r.status === 'fulfilled')
.map((r) => r.value);
}
Custom Async Utility Types
// Extract the resolved type from a Promise
type Awaited<T> = T extends Promise<infer U> ? U : T;
// Example usage
type UserPromise = Promise<User>;
type ResolvedUser = Awaited<UserPromise>; // User
// Type for an async function
type AsyncFunction<T extends unknown[], R> = (...args: T) => Promise<R>;
// Usage
const fetchUserTyped: AsyncFunction<[number], User> = async (id) => {
return fetchUser(id);
};
Typing Async Iterators
For streaming data or paginated APIs, use async iterators:
interface Page<T> {
items: T[];
nextCursor: string | null;
}
async function* fetchAllPages<T>(
fetchPage: (cursor?: string) => Promise<Page<T>>
): AsyncGenerator<T, void, undefined> {
let cursor: string | undefined;
while (true) {
const page = await fetchPage(cursor);
for (const item of page.items) {
yield item;
}
if (!page.nextCursor) break;
cursor = page.nextCursor;
}
}
// Usage
async function processAllUsers(): Promise<void> {
for await (const user of fetchAllPages<User>(fetchUserPage)) {
console.log(user.name);
}
}
Common Type Patterns
Conditional Async Returns
async function fetchOptional<T>(url: string, required: boolean): Promise<T | null> {
try {
const response = await fetch(url);
if (!response.ok && !required) {
return null;
}
return response.json();
} catch (error) {
if (required) throw error;
return null;
}
}
Overloaded Async Functions
// Overload signatures
async function getData(id: number): Promise<User>;
async function getData(ids: number[]): Promise<User[]>;
async function getData(idOrIds: number | number[]): Promise<User | User[]> {
if (Array.isArray(idOrIds)) {
return Promise.all(idOrIds.map((id) => fetchUser(id)));
}
return fetchUser(idOrIds);
}
// Usage
const user = await getData(1); // User
const users = await getData([1, 2]); // User[]
Exercise
Create a typed async cache utility with the following requirements:
- Generic type parameter for cached values
- Async fetch function when cache misses
- Expiration time support
- Type-safe get and set operations
Solution:
interface CacheEntry<T> {
value: T;
expiresAt: number;
}
class AsyncCache<T> {
private cache: Map<string, CacheEntry<T>> = new Map();
constructor(private defaultTtlMs: number = 60000) {}
async get(key: string, fetcher: () => Promise<T>, ttlMs?: number): Promise<T> {
const cached = this.cache.get(key);
if (cached && cached.expiresAt > Date.now()) {
return cached.value;
}
const value = await fetcher();
this.set(key, value, ttlMs);
return value;
}
set(key: string, value: T, ttlMs?: number): void {
this.cache.set(key, {
value,
expiresAt: Date.now() + (ttlMs ?? this.defaultTtlMs),
});
}
delete(key: string): boolean {
return this.cache.delete(key);
}
clear(): void {
this.cache.clear();
}
}
// Usage
const userCache = new AsyncCache<User>(300000); // 5 min default TTL
const user = await userCache.get(`user:${userId}`, () => fetchUser(userId));
Key Takeaways
- Async functions always return
Promise<T>- TypeScript handles this automatically - Explicit return types on public functions prevent
anyfrom sneaking in - Use generics to create reusable, type-safe async utilities
- Caught errors are
unknown- use type guards to narrow them Promise.allpreserves tuple types for typed destructuring- Create custom type guards for domain-specific error types
Resources
| Resource | Type | Level |
|---|---|---|
| TypeScript Handbook: Generics | Documentation | Intermediate |
| TypeScript Handbook: Narrowing | Documentation | Intermediate |
| TypeScript: Everyday Types | Documentation | Beginner |
Next Lesson
Continue to Lesson 3.5: Practice - Async Data Loader to build a complete, typed async data loader applying all concepts from this module.