Skip to main content

Async Programming in TypeScript

TypeScript adds type safety to JavaScript's asynchronous programming features, making it easier to work with Promises, async/await, and callbacks.

Promises

TypeScript can type Promise results:

// Basic Promise type
const fetchData: Promise<string> = new Promise((resolve) => {
setTimeout(() => resolve("Hello"), 1000);
});

// Function returning a Promise
async function getData(): Promise<string> {
const response = await fetch('https://api.example.com/data');
return response.text();
}

Async/Await with Types

Type-safe async operations:

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

async function getUser(id: number): Promise<User> {
const response = await fetch(`/api/users/${id}`);
if (!response.ok) {
throw new Error('User not found');
}
return response.json();
}

// Using the async function
try {
const user = await getUser(1);
console.log(user.name); // TypeScript knows user has name
} catch (error) {
console.error(error);
}

Error Handling

Type-safe error handling in async code:

class APIError extends Error {
constructor(
public statusCode: number,
message: string
) {
super(message);
}
}

async function fetchUser(id: number): Promise<User> {
try {
const response = await fetch(`/api/users/${id}`);
if (!response.ok) {
throw new APIError(response.status, 'Failed to fetch user');
}
return response.json();
} catch (error) {
if (error instanceof APIError) {
// Handle API-specific error
console.error(`API Error ${error.statusCode}: ${error.message}`);
}
throw error;
}
}

Concurrent Operations

Type-safe parallel execution:

interface Task {
id: string;
result: number;
}

async function processTasks(tasks: Task[]): Promise<number[]> {
// Process all tasks concurrently
const promises = tasks.map(async (task) => {
const result = await processTask(task);
return result;
});

// Wait for all to complete
return Promise.all(promises);
}

// Using with type inference
const results = await processTasks([
{ id: "1", result: 42 },
{ id: "2", result: 84 }
]);

Best Practices

  1. Always specify Promise return types
  2. Use try/catch blocks for error handling
  3. Consider Promise.all for concurrent operations
  4. Handle edge cases and timeouts
  5. Keep async functions focused and small

Common Patterns

Loading States

interface State<T> {
data: T | null;
loading: boolean;
error: Error | null;
}

function createInitialState<T>(): State<T> {
return {
data: null,
loading: false,
error: null
};
}

async function fetchWithState<T>(
url: string,
state: State<T>
): Promise<State<T>> {
try {
state.loading = true;
const response = await fetch(url);
state.data = await response.json();
return state;
} catch (error) {
state.error = error as Error;
return state;
} finally {
state.loading = false;
}
}

Practice Exercise

Create a type-safe async task queue that:

  1. Handles different types of tasks
  2. Processes tasks in order
  3. Includes proper error handling
  4. Reports progress with TypeScript types