Skip to main content

TypeScript AJAX

Introduction

AJAX (Asynchronous JavaScript and XML) is a technique for creating dynamic web applications that can update content without reloading the entire page. When combined with TypeScript's strong typing system, AJAX operations become more robust and less error-prone.

In this tutorial, you'll learn how to implement AJAX requests in TypeScript using different methods, handle responses properly, and structure your code for maintainable applications.

What is AJAX?

AJAX allows web applications to:

  • Send data to a server in the background
  • Receive data from a server in the background
  • Update portions of a web page without reloading the whole page

Despite having "XML" in its name, modern AJAX implementations typically use JSON rather than XML for data transfer.

TypeScript Advantages for AJAX

TypeScript brings several benefits when working with AJAX:

  • Type safety for request and response data
  • Interfaces to model API responses
  • Error handling with type information
  • Intellisense and autocompletion for API endpoints and options

Making AJAX Requests in TypeScript

Let's explore the most common ways to make AJAX requests in TypeScript.

1. Using Fetch API

The Fetch API is built into modern browsers and provides a powerful way to make HTTP requests.

First, let's define an interface for our response:

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

Now, let's make a simple fetch request:

typescript
function fetchUsers(): Promise<User[]> {
return fetch('https://api.example.com/users')
.then(response => {
if (!response.ok) {
throw new Error(`HTTP error! Status: ${response.status}`);
}
return response.json() as Promise<User[]>;
});
}

// Usage
fetchUsers()
.then(users => {
console.log('Users:', users);
// Do something with the users
users.forEach(user => {
console.log(`User ${user.id}: ${user.name} (${user.email})`);
});
})
.catch(error => {
console.error('Error fetching users:', error);
});

2. Using async/await with Fetch

We can improve the readability of the previous example using async/await:

typescript
async function fetchUsers(): Promise<User[]> {
try {
const response = await fetch('https://api.example.com/users');

if (!response.ok) {
throw new Error(`HTTP error! Status: ${response.status}`);
}

const users = await response.json() as User[];
return users;
} catch (error) {
console.error('Error in fetchUsers:', error);
throw error;
}
}

// Usage
async function displayUsers() {
try {
const users = await fetchUsers();
users.forEach(user => {
console.log(`User ${user.id}: ${user.name} (${user.email})`);
});
} catch (error) {
console.error('Failed to display users:', error);
}
}

// Call the function
displayUsers();

3. Using Axios with TypeScript

Axios is a popular HTTP client that works well with TypeScript. To use it, you'll need to install it:

bash
npm install axios

Here's how to use Axios with TypeScript:

typescript
import axios from 'axios';

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

// Get request
async function getUsers(): Promise<User[]> {
try {
const response = await axios.get<User[]>('https://api.example.com/users');
return response.data;
} catch (error) {
console.error('Error fetching users:', error);
throw error;
}
}

// Post request
async function createUser(user: Omit<User, 'id'>): Promise<User> {
try {
const response = await axios.post<User>('https://api.example.com/users', user);
return response.data;
} catch (error) {
console.error('Error creating user:', error);
throw error;
}
}

// Usage
async function manageUsers() {
try {
// Get all users
const users = await getUsers();
console.log('All users:', users);

// Create a new user
const newUser = await createUser({
name: 'John Doe',
email: '[email protected]'
});
console.log('New user created:', newUser);
} catch (error) {
console.error('User management failed:', error);
}
}

manageUsers();

Handling Different Response Types

Let's create more complex examples dealing with different response types.

Paginated API Responses

typescript
interface PaginatedResponse<T> {
data: T[];
page: number;
totalPages: number;
totalItems: number;
}

interface Product {
id: string;
name: string;
price: number;
category: string;
}

async function fetchProducts(page: number = 1): Promise<PaginatedResponse<Product>> {
try {
const response = await axios.get<PaginatedResponse<Product>>(
`https://api.example.com/products?page=${page}`
);
return response.data;
} catch (error) {
console.error('Error fetching products:', error);
throw error;
}
}

// Usage
async function displayProductsByPage() {
let currentPage = 1;
let hasMorePages = true;

while (hasMorePages) {
const result = await fetchProducts(currentPage);

console.log(`Page ${currentPage} of ${result.totalPages}`);
result.data.forEach(product => {
console.log(`${product.name} - $${product.price}`);
});

hasMorePages = currentPage < result.totalPages;
currentPage++;
}
}

Error Handling in TypeScript AJAX

Proper error handling is crucial for AJAX operations. Let's create a more structured approach:

typescript
// Custom error class
class ApiError extends Error {
statusCode: number;

constructor(message: string, statusCode: number) {
super(message);
this.name = 'ApiError';
this.statusCode = statusCode;
}
}

async function fetchWithErrorHandling<T>(url: string): Promise<T> {
try {
const response = await fetch(url);

if (!response.ok) {
throw new ApiError(`API request failed with status: ${response.status}`, response.status);
}

return await response.json() as T;
} catch (error) {
// Re-throw ApiError objects as is
if (error instanceof ApiError) {
throw error;
}

// Convert other errors to ApiError
if (error instanceof Error) {
throw new ApiError(error.message, 0);
}

// For unknown errors
throw new ApiError('Unknown error occurred', 0);
}
}

// Usage
interface UserProfile {
id: number;
username: string;
profile: {
fullName: string;
bio: string;
};
}

async function getUserProfile(userId: number): Promise<UserProfile> {
try {
return await fetchWithErrorHandling<UserProfile>(`https://api.example.com/users/${userId}`);
} catch (error) {
if (error instanceof ApiError) {
if (error.statusCode === 404) {
console.error(`User with ID ${userId} not found.`);
} else if (error.statusCode >= 500) {
console.error("Server error. Please try again later.");
} else {
console.error(`API Error: ${error.message}`);
}
} else {
console.error("An unexpected error occurred:", error);
}
throw error;
}
}

Creating a Reusable API Service

For larger applications, it's useful to create a reusable API service:

typescript
import axios, { AxiosInstance, AxiosRequestConfig, AxiosResponse } from 'axios';

// Basic interface for API responses
interface ApiResponse<T> {
data: T;
success: boolean;
message?: string;
}

class ApiService {
private client: AxiosInstance;

constructor(baseURL: string) {
this.client = axios.create({
baseURL,
headers: {
'Content-Type': 'application/json'
}
});

// Request interceptor - add auth token if available
this.client.interceptors.request.use((config) => {
const token = localStorage.getItem('auth_token');
if (token && config.headers) {
config.headers.Authorization = `Bearer ${token}`;
}
return config;
});

// Response interceptor - handle common errors
this.client.interceptors.response.use(
(response) => response,
(error) => {
if (error.response?.status === 401) {
// Handle unauthorized - maybe redirect to login
console.log('User session expired. Redirecting to login...');
// window.location.href = '/login';
}
return Promise.reject(error);
}
);
}

async get<T>(url: string, config?: AxiosRequestConfig): Promise<T> {
const response = await this.client.get<ApiResponse<T>>(url, config);
return response.data.data;
}

async post<T>(url: string, data?: any, config?: AxiosRequestConfig): Promise<T> {
const response = await this.client.post<ApiResponse<T>>(url, data, config);
return response.data.data;
}

async put<T>(url: string, data?: any, config?: AxiosRequestConfig): Promise<T> {
const response = await this.client.put<ApiResponse<T>>(url, data, config);
return response.data.data;
}

async delete<T>(url: string, config?: AxiosRequestConfig): Promise<T> {
const response = await this.client.delete<ApiResponse<T>>(url, config);
return response.data.data;
}
}

// Usage example
const api = new ApiService('https://api.example.com');

interface Todo {
id: number;
title: string;
completed: boolean;
}

// Using the service
async function fetchTodos(): Promise<Todo[]> {
return await api.get<Todo[]>('/todos');
}

async function createTodo(title: string): Promise<Todo> {
return await api.post<Todo>('/todos', { title, completed: false });
}

async function updateTodo(id: number, data: Partial<Todo>): Promise<Todo> {
return await api.put<Todo>(`/todos/${id}`, data);
}

async function removeTodo(id: number): Promise<void> {
return await api.delete<void>(`/todos/${id}`);
}

A Real-world Example: Building a Weather Dashboard

Let's create a more complete example of a weather dashboard application:

typescript
// Weather API interfaces
interface WeatherLocation {
name: string;
country: string;
lat: number;
lon: number;
}

interface WeatherData {
location: WeatherLocation;
current: {
temp_c: number;
temp_f: number;
condition: {
text: string;
icon: string;
};
humidity: number;
wind_kph: number;
};
forecast: {
forecastday: Array<{
date: string;
day: {
avgtemp_c: number;
condition: {
text: string;
icon: string;
}
}
}>;
};
}

class WeatherService {
private apiKey: string;
private baseUrl: string = 'https://api.weatherapi.com/v1';

constructor(apiKey: string) {
this.apiKey = apiKey;
}

async getWeather(location: string): Promise<WeatherData> {
try {
const url = `${this.baseUrl}/forecast.json?key=${this.apiKey}&q=${encodeURIComponent(location)}&days=3`;
const response = await fetch(url);

if (!response.ok) {
throw new Error(`Weather API error: ${response.status}`);
}

return await response.json() as WeatherData;
} catch (error) {
console.error('Failed to fetch weather data:', error);
throw error;
}
}
}

class WeatherApp {
private weatherService: WeatherService;

constructor(apiKey: string) {
this.weatherService = new WeatherService(apiKey);
this.init();
}

private init(): void {
const searchForm = document.getElementById('weather-search') as HTMLFormElement;

if (searchForm) {
searchForm.addEventListener('submit', async (e) => {
e.preventDefault();
const input = document.getElementById('location-input') as HTMLInputElement;
if (input && input.value) {
await this.displayWeather(input.value);
}
});
}
}

private async displayWeather(location: string): Promise<void> {
try {
const weatherContainer = document.getElementById('weather-container');

if (weatherContainer) {
weatherContainer.innerHTML = '<p>Loading weather data...</p>';

const data = await this.weatherService.getWeather(location);

const html = `
<div class="weather-card">
<h2>${data.location.name}, ${data.location.country}</h2>
<div class="current-weather">
<img src="${data.current.condition.icon}" alt="${data.current.condition.text}">
<div class="temperature">
<span class="temp">${data.current.temp_c}°C / ${data.current.temp_f}°F</span>
<span class="condition">${data.current.condition.text}</span>
</div>
<div class="details">
<p>Humidity: ${data.current.humidity}%</p>
<p>Wind: ${data.current.wind_kph} km/h</p>
</div>
</div>
<div class="forecast">
<h3>3-Day Forecast</h3>
<div class="forecast-days">
${data.forecast.forecastday.map(day => `
<div class="forecast-day">
<p>${new Date(day.date).toLocaleDateString('en-US', { weekday: 'short' })}</p>
<img src="${day.day.condition.icon}" alt="${day.day.condition.text}">
<p>${day.day.avgtemp_c}°C</p>
</div>
`).join('')}
</div>
</div>
</div>
`;

weatherContainer.innerHTML = html;
}
} catch (error) {
console.error('Error displaying weather:', error);
const weatherContainer = document.getElementById('weather-container');
if (weatherContainer) {
weatherContainer.innerHTML = '<p class="error">Failed to load weather data. Please try again.</p>';
}
}
}
}

// Initialize the app when the DOM is loaded
document.addEventListener('DOMContentLoaded', () => {
const apiKey = 'your_api_key_here'; // Replace with your actual API key
new WeatherApp(apiKey);
});

This example would work with the following HTML:

html
<div id="weather-app">
<form id="weather-search">
<input type="text" id="location-input" placeholder="Enter city name" />
<button type="submit">Get Weather</button>
</form>
<div id="weather-container"></div>
</div>

Best Practices for AJAX in TypeScript

  1. Define interfaces for API responses: Always define TypeScript interfaces for your API responses to leverage type checking.

  2. Handle errors properly: Use try/catch blocks with async/await or catch statements with promises.

  3. Use generic types: Make your fetch functions reusable with generic types.

  4. Cancel unnecessary requests: Use AbortController to cancel requests that are no longer needed.

  5. Cache responses: Consider caching responses to minimize API calls.

Here's an example of implementing request cancellation:

typescript
function fetchWithCancellation<T>(url: string, signal: AbortSignal): Promise<T> {
return fetch(url, { signal })
.then(response => {
if (!response.ok) {
throw new Error(`HTTP error! Status: ${response.status}`);
}
return response.json() as Promise<T>;
});
}

// Usage
const controller = new AbortController();
const signal = controller.signal;

// Start the fetch
fetchWithCancellation<User[]>('https://api.example.com/users', signal)
.then(users => {
console.log('Users:', users);
})
.catch(err => {
if (err.name === 'AbortError') {
console.log('Fetch was cancelled');
} else {
console.error('Error:', err);
}
});

// Cancel the fetch after 2 seconds
setTimeout(() => {
controller.abort();
console.log('Fetch aborted');
}, 2000);

Summary

In this tutorial, we've covered:

  • Making AJAX requests in TypeScript using Fetch API and Axios
  • Creating interfaces for type-safe API responses
  • Handling errors in AJAX requests
  • Building reusable API services
  • Implementing a real-world example with a weather dashboard
  • Best practices for AJAX in TypeScript applications

TypeScript brings strong typing to AJAX operations, making your API interactions more reliable and maintainable. By defining interfaces for your API responses and implementing proper error handling, you can create robust web applications that effectively communicate with backend services.

Additional Resources

Exercises

  1. Create a simple Todo application that uses TypeScript and AJAX to interact with a REST API.

  2. Implement a generic function that can handle paginated API responses with proper TypeScript interfaces.

  3. Build a reusable API service class that includes authentication, request caching, and error handling.

  4. Create a TypeScript interface for a complex nested API response and implement functions to fetch and process the data.

  5. Implement request cancellation for a search feature where new requests should cancel previous pending requests.



If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)