Prerequisites
- Basic TypeScript syntax and types ๐
- Understanding of JavaScript Promises ๐ค
- Async/await fundamentals โฐ
What you'll learn
- Master Promise<T> types and generic constraints โก
- Handle async errors with complete type safety ๐ก๏ธ
- Build robust async workflows and patterns ๐๏ธ
- Create type-safe promise utilities and helpers โจ
๐ฏ Introduction
Welcome to the exciting world of type-safe asynchronous programming! ๐ In this guide, weโll explore how TypeScript transforms JavaScript Promises from potential sources of runtime errors into bulletproof, type-safe async operations.
Youโll discover how to harness TypeScriptโs powerful type system to catch async errors at compile time, create robust data flows, and build applications that handle uncertainty with confidence. Whether youโre fetching data from APIs ๐, processing files ๐, or orchestrating complex async workflows ๐, mastering typed Promises is essential for modern TypeScript development.
By the end of this tutorial, youโll be writing async code thatโs not just functional, but elegantly typed and impossible to break! ๐ Letโs dive in! ๐โโ๏ธ
๐ Understanding TypeScript Promises
๐ค What are Typed Promises?
Think of a TypeScript Promise as a type-safe container for future values ๐ฆ. Itโs like placing an order at a restaurant ๐ - you get a receipt (the Promise) that guarantees youโll eventually receive your specific meal (the typed result), or youโll be told what went wrong (the typed error).
In TypeScript terms, Promise<T>
provides:
- โจ Type-safe results - know exactly what youโll get when the promise resolves
- ๐ Compile-time error checking - catch async mistakes before runtime
- ๐ก๏ธ Intelligent autocomplete - IDE support for async return values
- ๐ฆ Generic constraints - ensure promises return the right types
๐ก Why Use Typed Promises?
Hereโs why typed promises are game-changers:
- Predictable Async Code ๐ฎ: Know what your async functions return
- Early Error Detection ๐จ: Catch type mismatches before deployment
- Safer API Calls ๐: Ensure responses match expected interfaces
- Better Team Collaboration ๐ฅ: Self-documenting async interfaces
- Refactoring Confidence ๐ง: Change async code without fear
Real-world example: When fetching user data from an API ๐ค, typed promises ensure you canโt accidentally treat a User object as a string!
๐ง Basic Promise Types and Patterns
๐ Promise<T> Fundamentals
Letโs start with the core Promise types:
// ๐ฏ Basic Promise types
// Promise that resolves to a string
const fetchMessage = (): Promise<string> => {
return new Promise((resolve) => {
setTimeout(() => {
resolve("Hello, TypeScript! ๐");
}, 1000);
});
};
// Promise that resolves to a number
const calculateAsync = (a: number, b: number): Promise<number> => {
return new Promise((resolve, reject) => {
if (a < 0 || b < 0) {
reject(new Error("Negative numbers not allowed! ๐ซ"));
return;
}
// ๐งฎ Simulate async calculation
setTimeout(() => {
const result = a * b + Math.random() * 100;
resolve(Math.round(result));
}, 500);
});
};
// Promise that resolves to a custom interface
interface UserProfile {
id: string;
name: string;
email: string;
avatar?: string;
lastLogin: Date;
}
const fetchUserProfile = (userId: string): Promise<UserProfile> => {
return new Promise((resolve, reject) => {
// ๐ Simulate API call validation
if (!userId || userId.trim() === '') {
reject(new Error("User ID is required! ๐"));
return;
}
// ๐ Mock successful response
setTimeout(() => {
const user: UserProfile = {
id: userId,
name: `User ${userId}`,
email: `user${userId}@example.com`,
avatar: `https://api.adorable.io/avatars/100/${userId}.png`,
lastLogin: new Date()
};
resolve(user);
console.log(`โ
Fetched profile for user: ${user.name}`);
}, 800);
});
};
๐ก Explanation: Notice how each Promise is typed with Promise<T>
where T is the exact type of data it will resolve to!
๐ฏ Error Handling with Types
TypeScript provides excellent support for typed error handling:
// ๐จ Custom error types for better error handling
class NetworkError extends Error {
constructor(
message: string,
public statusCode: number,
public endpoint: string
) {
super(message);
this.name = 'NetworkError';
}
}
class ValidationError extends Error {
constructor(
message: string,
public field: string,
public value: any
) {
super(message);
this.name = 'ValidationError';
}
}
// ๐ฏ Type-safe promise with specific error types
const fetchDataWithTypedErrors = async (url: string): Promise<any> => {
try {
if (!url.startsWith('http')) {
throw new ValidationError(
'Invalid URL format ๐',
'url',
url
);
}
// ๐ก Simulate HTTP request
const response = await fetch(url);
if (!response.ok) {
throw new NetworkError(
`HTTP ${response.status}: ${response.statusText} ๐ก`,
response.status,
url
);
}
const data = await response.json();
console.log(`โ
Successfully fetched data from: ${url}`);
return data;
} catch (error) {
// ๐ฏ TypeScript helps us handle different error types
if (error instanceof NetworkError) {
console.error(`๐ Network error (${error.statusCode}): ${error.message}`);
throw error;
} else if (error instanceof ValidationError) {
console.error(`โ ๏ธ Validation error for ${error.field}: ${error.message}`);
throw error;
} else {
console.error(`๐ฅ Unexpected error: ${error}`);
throw new Error(`Unexpected error: ${error}`);
}
}
};
// ๐ก๏ธ Result type for safer error handling
type Result<T, E = Error> = {
success: true;
data: T;
} | {
success: false;
error: E;
};
// ๐ฏ Safe promise wrapper that never throws
const safePromise = async <T>(
promise: Promise<T>
): Promise<Result<T>> => {
try {
const data = await promise;
return { success: true, data };
} catch (error) {
return {
success: false,
error: error instanceof Error ? error : new Error(String(error))
};
}
};
๐ Promise Composition and Chaining
Advanced patterns for combining promises:
// ๐ Promise chaining with type safety
interface ApiUser {
id: string;
username: string;
email: string;
}
interface UserPosts {
userId: string;
posts: Array<{
id: string;
title: string;
content: string;
createdAt: Date;
}>;
}
interface UserStats {
userId: string;
totalPosts: number;
averageWordsPerPost: number;
lastPostDate: Date | null;
}
// ๐ฏ Chained API calls with type preservation
const getUserWithPostsAndStats = async (userId: string): Promise<{
user: ApiUser;
posts: UserPosts;
stats: UserStats;
}> => {
// ๐ค First, get the user
const user = await fetchApiUser(userId);
console.log(`๐ Fetched user: ${user.username}`);
// ๐ Then get their posts
const posts = await fetchUserPosts(user.id);
console.log(`๐ Fetched ${posts.posts.length} posts`);
// ๐ Finally calculate stats
const stats = await calculateUserStats(posts);
console.log(`๐ Calculated stats: ${stats.totalPosts} posts`);
return { user, posts, stats };
};
// ๐ญ Mock API functions with proper typing
const fetchApiUser = (userId: string): Promise<ApiUser> => {
return new Promise((resolve, reject) => {
if (!userId) {
reject(new Error("User ID required! ๐"));
return;
}
setTimeout(() => {
resolve({
id: userId,
username: `user_${userId}`,
email: `${userId}@example.com`
});
}, 300);
});
};
const fetchUserPosts = (userId: string): Promise<UserPosts> => {
return new Promise((resolve) => {
setTimeout(() => {
const posts = Array.from({ length: 5 }, (_, i) => ({
id: `post_${i + 1}`,
title: `Post ${i + 1} by user ${userId}`,
content: `This is the content of post ${i + 1}. Lorem ipsum dolor sit amet.`,
createdAt: new Date(Date.now() - (i * 24 * 60 * 60 * 1000))
}));
resolve({ userId, posts });
}, 500);
});
};
const calculateUserStats = (userPosts: UserPosts): Promise<UserStats> => {
return new Promise((resolve) => {
setTimeout(() => {
const { posts } = userPosts;
const totalPosts = posts.length;
const totalWords = posts.reduce((sum, post) => {
return sum + post.content.split(' ').length;
}, 0);
const averageWordsPerPost = totalPosts > 0 ? totalWords / totalPosts : 0;
const lastPostDate = posts.length > 0 ? posts[0].createdAt : null;
resolve({
userId: userPosts.userId,
totalPosts,
averageWordsPerPost: Math.round(averageWordsPerPost),
lastPostDate
});
}, 200);
});
};
๐ก Practical Examples
๐ Example 1: Type-Safe API Client
Letโs build a comprehensive, type-safe API client:
// ๐ก Generic API response types
interface ApiResponse<T> {
success: boolean;
data: T;
message: string;
timestamp: Date;
}
interface ApiError {
success: false;
error: {
code: string;
message: string;
details?: Record<string, any>;
};
timestamp: Date;
}
// ๐ฏ HTTP methods enum
enum HttpMethod {
GET = 'GET',
POST = 'POST',
PUT = 'PUT',
DELETE = 'DELETE',
PATCH = 'PATCH'
}
// ๐ง Request configuration interface
interface RequestConfig {
method: HttpMethod;
headers?: Record<string, string>;
body?: any;
timeout?: number;
retries?: number;
}
// ๐ Type-safe API client class
class TypeSafeApiClient {
private baseUrl: string;
private defaultHeaders: Record<string, string>;
private defaultTimeout: number = 5000;
constructor(baseUrl: string, defaultHeaders: Record<string, string> = {}) {
this.baseUrl = baseUrl.replace(/\/$/, ''); // Remove trailing slash
this.defaultHeaders = {
'Content-Type': 'application/json',
...defaultHeaders
};
}
// ๐ฏ Generic request method with full type safety
private async request<T>(
endpoint: string,
config: RequestConfig
): Promise<ApiResponse<T>> {
const url = `${this.baseUrl}${endpoint}`;
const timeout = config.timeout || this.defaultTimeout;
console.log(`๐ก ${config.method} ${url}`);
try {
// ๐ง Prepare request options
const requestOptions: RequestInit = {
method: config.method,
headers: {
...this.defaultHeaders,
...config.headers
}
};
// ๐ฆ Add body for non-GET requests
if (config.body && config.method !== HttpMethod.GET) {
requestOptions.body = JSON.stringify(config.body);
}
// โฐ Create timeout promise
const timeoutPromise = new Promise<never>((_, reject) => {
setTimeout(() => {
reject(new Error(`Request timeout after ${timeout}ms โฐ`));
}, timeout);
});
// ๐โโ๏ธ Race between fetch and timeout
const response = await Promise.race([
fetch(url, requestOptions),
timeoutPromise
]);
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText} ๐ก`);
}
const data = await response.json();
const apiResponse: ApiResponse<T> = {
success: true,
data,
message: 'Request successful',
timestamp: new Date()
};
console.log(`โ
${config.method} ${url} - Success`);
return apiResponse;
} catch (error) {
console.error(`โ ${config.method} ${url} - Failed:`, error);
throw error;
}
}
// ๐ GET request with type safety
async get<T>(endpoint: string, headers?: Record<string, string>): Promise<ApiResponse<T>> {
return this.request<T>(endpoint, {
method: HttpMethod.GET,
headers
});
}
// โ POST request with typed body and response
async post<TRequest, TResponse>(
endpoint: string,
body: TRequest,
headers?: Record<string, string>
): Promise<ApiResponse<TResponse>> {
return this.request<TResponse>(endpoint, {
method: HttpMethod.POST,
body,
headers
});
}
// ๐ PUT request with full typing
async put<TRequest, TResponse>(
endpoint: string,
body: TRequest,
headers?: Record<string, string>
): Promise<ApiResponse<TResponse>> {
return this.request<TResponse>(endpoint, {
method: HttpMethod.PUT,
body,
headers
});
}
// ๐๏ธ DELETE request
async delete<T>(endpoint: string, headers?: Record<string, string>): Promise<ApiResponse<T>> {
return this.request<T>(endpoint, {
method: HttpMethod.DELETE,
headers
});
}
// ๐ก๏ธ Safe request wrapper that doesn't throw
async safeRequest<T>(
method: 'get' | 'post' | 'put' | 'delete',
endpoint: string,
body?: any
): Promise<Result<ApiResponse<T>, ApiError>> {
try {
let result: ApiResponse<T>;
switch (method) {
case 'get':
result = await this.get<T>(endpoint);
break;
case 'post':
result = await this.post<any, T>(endpoint, body);
break;
case 'put':
result = await this.put<any, T>(endpoint, body);
break;
case 'delete':
result = await this.delete<T>(endpoint);
break;
}
return { success: true, data: result };
} catch (error) {
const apiError: ApiError = {
success: false,
error: {
code: 'REQUEST_FAILED',
message: error instanceof Error ? error.message : 'Unknown error',
details: { method, endpoint, body }
},
timestamp: new Date()
};
return { success: false, error: apiError };
}
}
}
// ๐ฎ Usage example with specific types
interface BlogPost {
id: string;
title: string;
content: string;
author: string;
publishedAt: Date;
tags: string[];
}
interface CreatePostRequest {
title: string;
content: string;
author: string;
tags: string[];
}
// ๐ Blog API service using the typed client
class BlogApiService {
private client: TypeSafeApiClient;
constructor(apiUrl: string, authToken?: string) {
const headers = authToken ? { 'Authorization': `Bearer ${authToken}` } : {};
this.client = new TypeSafeApiClient(apiUrl, headers);
}
// ๐ Get all posts with type safety
async getAllPosts(): Promise<BlogPost[]> {
const response = await this.client.get<BlogPost[]>('/posts');
return response.data;
}
// ๐๏ธ Get single post by ID
async getPost(id: string): Promise<BlogPost> {
const response = await this.client.get<BlogPost>(`/posts/${id}`);
return response.data;
}
// โ Create new post
async createPost(postData: CreatePostRequest): Promise<BlogPost> {
const response = await this.client.post<CreatePostRequest, BlogPost>(
'/posts',
postData
);
return response.data;
}
// ๐ Update existing post
async updatePost(id: string, postData: Partial<CreatePostRequest>): Promise<BlogPost> {
const response = await this.client.put<Partial<CreatePostRequest>, BlogPost>(
`/posts/${id}`,
postData
);
return response.data;
}
// ๐๏ธ Delete post
async deletePost(id: string): Promise<{ deleted: boolean }> {
const response = await this.client.delete<{ deleted: boolean }>(`/posts/${id}`);
return response.data;
}
}
๐ฏ Try it yourself: Add caching, request deduplication, and retry logic to the API client!
๐ Example 2: File Processing Pipeline
Letโs create a type-safe file processing system:
// ๐ File processing types
interface FileMetadata {
name: string;
size: number;
type: string;
lastModified: Date;
path: string;
}
interface ProcessingResult<T> {
file: FileMetadata;
result: T;
processingTime: number;
success: boolean;
}
interface ImageProcessingOptions {
width?: number;
height?: number;
quality?: number;
format?: 'jpeg' | 'png' | 'webp';
}
interface ProcessedImage {
originalSize: number;
processedSize: number;
dimensions: {
width: number;
height: number;
};
format: string;
outputPath: string;
}
// ๐ญ Type-safe file processing pipeline
class FileProcessingPipeline {
private processingQueue: Map<string, Promise<any>> = new Map();
// ๐ Process multiple files with type safety
async processFiles<T>(
files: FileMetadata[],
processor: (file: FileMetadata) => Promise<T>,
options: {
concurrency?: number;
onProgress?: (completed: number, total: number) => void;
onError?: (file: FileMetadata, error: Error) => void;
} = {}
): Promise<ProcessingResult<T>[]> {
const { concurrency = 3, onProgress, onError } = options;
const results: ProcessingResult<T>[] = [];
console.log(`๐ญ Starting processing pipeline for ${files.length} files`);
// ๐ Process files in batches to control concurrency
for (let i = 0; i < files.length; i += concurrency) {
const batch = files.slice(i, i + concurrency);
const batchPromises = batch.map(async (file): Promise<ProcessingResult<T>> => {
const startTime = Date.now();
try {
console.log(`โ๏ธ Processing: ${file.name}`);
const result = await processor(file);
const processingTime = Date.now() - startTime;
console.log(`โ
Completed: ${file.name} (${processingTime}ms)`);
return {
file,
result,
processingTime,
success: true
};
} catch (error) {
const processingTime = Date.now() - startTime;
console.error(`โ Failed: ${file.name}`, error);
if (onError) {
onError(file, error instanceof Error ? error : new Error(String(error)));
}
return {
file,
result: null as any, // This will be filtered out
processingTime,
success: false
};
}
});
const batchResults = await Promise.allSettled(batchPromises);
// ๐ Extract successful results
for (const result of batchResults) {
if (result.status === 'fulfilled' && result.value.success) {
results.push(result.value);
}
}
// ๐ Report progress
if (onProgress) {
onProgress(results.length, files.length);
}
}
console.log(`๐ Pipeline completed: ${results.length}/${files.length} files processed`);
return results;
}
// ๐ผ๏ธ Image-specific processing
async processImages(
imageFiles: FileMetadata[],
options: ImageProcessingOptions = {}
): Promise<ProcessingResult<ProcessedImage>[]> {
return this.processFiles(
imageFiles.filter(file => file.type.startsWith('image/')),
(file) => this.processImage(file, options),
{
concurrency: 2, // Images are resource-intensive
onProgress: (completed, total) => {
console.log(`๐ผ๏ธ Image processing: ${completed}/${total}`);
},
onError: (file, error) => {
console.error(`โ Image processing failed for ${file.name}:`, error.message);
}
}
);
}
// ๐จ Single image processor
private async processImage(
file: FileMetadata,
options: ImageProcessingOptions
): Promise<ProcessedImage> {
// ๐ญ Simulate image processing
return new Promise((resolve, reject) => {
if (file.size > 50 * 1024 * 1024) { // 50MB limit
reject(new Error(`File too large: ${file.size} bytes ๐`));
return;
}
setTimeout(() => {
const processed: ProcessedImage = {
originalSize: file.size,
processedSize: Math.round(file.size * 0.7), // 30% compression
dimensions: {
width: options.width || 1920,
height: options.height || 1080
},
format: options.format || 'jpeg',
outputPath: `/processed/${file.name.replace(/\.[^.]+$/, `.${options.format || 'jpg'}`)}`
};
resolve(processed);
}, Math.random() * 2000 + 500); // 0.5-2.5s processing time
});
}
// ๐ Get processing statistics
async getProcessingStats(results: ProcessingResult<any>[]): Promise<{
totalFiles: number;
successfulFiles: number;
failedFiles: number;
averageProcessingTime: number;
totalProcessingTime: number;
}> {
return new Promise((resolve) => {
const successful = results.filter(r => r.success);
const totalTime = results.reduce((sum, r) => sum + r.processingTime, 0);
resolve({
totalFiles: results.length,
successfulFiles: successful.length,
failedFiles: results.length - successful.length,
averageProcessingTime: results.length > 0 ? Math.round(totalTime / results.length) : 0,
totalProcessingTime: totalTime
});
});
}
}
// ๐ฎ Usage example
const pipeline = new FileProcessingPipeline();
// ๐ Mock file metadata
const sampleFiles: FileMetadata[] = [
{
name: 'vacation-photo.jpg',
size: 2.5 * 1024 * 1024, // 2.5MB
type: 'image/jpeg',
lastModified: new Date(),
path: '/uploads/vacation-photo.jpg'
},
{
name: 'profile-picture.png',
size: 1.2 * 1024 * 1024, // 1.2MB
type: 'image/png',
lastModified: new Date(),
path: '/uploads/profile-picture.png'
},
{
name: 'presentation.pptx',
size: 15 * 1024 * 1024, // 15MB
type: 'application/vnd.openxmlformats-officedocument.presentationml.presentation',
lastModified: new Date(),
path: '/uploads/presentation.pptx'
}
];
// ๐ Process images with type safety
async function runImageProcessing(): Promise<void> {
try {
const results = await pipeline.processImages(sampleFiles, {
width: 1200,
height: 800,
quality: 85,
format: 'jpeg'
});
console.log(`๐ฏ Processed ${results.length} images successfully`);
const stats = await pipeline.getProcessingStats(results);
console.log('๐ Processing Statistics:', stats);
} catch (error) {
console.error('โ Pipeline failed:', error);
}
}
๐ Advanced Promise Patterns
๐งโโ๏ธ Promise Utilities and Helpers
Advanced utilities for promise composition:
// ๐ฏ Advanced Promise utility class
class PromiseUtils {
// โฐ Timeout wrapper for any promise
static withTimeout<T>(
promise: Promise<T>,
timeoutMs: number,
timeoutMessage?: string
): Promise<T> {
const timeoutPromise = new Promise<never>((_, reject) => {
setTimeout(() => {
reject(new Error(timeoutMessage || `Operation timed out after ${timeoutMs}ms โฐ`));
}, timeoutMs);
});
return Promise.race([promise, timeoutPromise]);
}
// ๐ Retry failed promises with exponential backoff
static async retry<T>(
promiseFactory: () => Promise<T>,
options: {
maxAttempts?: number;
baseDelay?: number;
maxDelay?: number;
backoffFactor?: number;
shouldRetry?: (error: Error) => boolean;
} = {}
): Promise<T> {
const {
maxAttempts = 3,
baseDelay = 1000,
maxDelay = 10000,
backoffFactor = 2,
shouldRetry = () => true
} = options;
let lastError: Error;
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
try {
console.log(`๐ Attempt ${attempt}/${maxAttempts}`);
return await promiseFactory();
} catch (error) {
lastError = error instanceof Error ? error : new Error(String(error));
if (attempt === maxAttempts || !shouldRetry(lastError)) {
break;
}
// ๐ Calculate delay with exponential backoff
const delay = Math.min(
baseDelay * Math.pow(backoffFactor, attempt - 1),
maxDelay
);
console.log(`โณ Waiting ${delay}ms before retry...`);
await new Promise(resolve => setTimeout(resolve, delay));
}
}
throw lastError!;
}
// ๐๏ธ Parallel promise execution with concurrency limit
static async parallel<T>(
tasks: (() => Promise<T>)[],
concurrency: number = 5
): Promise<T[]> {
const results: T[] = [];
const executing: Promise<void>[] = [];
for (const task of tasks) {
const promise = task().then(result => {
results.push(result);
});
executing.push(promise);
if (executing.length >= concurrency) {
await Promise.race(executing);
// Remove completed promises
executing.splice(0, executing.length);
}
}
// Wait for remaining promises
await Promise.all(executing);
return results;
}
// ๐ Chain promises in sequence
static async sequence<T>(
tasks: (() => Promise<T>)[]
): Promise<T[]> {
const results: T[] = [];
for (const task of tasks) {
const result = await task();
results.push(result);
}
return results;
}
// ๐ฐ Promise with random delay (useful for testing)
static delay<T>(value: T, minMs: number = 0, maxMs?: number): Promise<T> {
const delay = maxMs ? Math.random() * (maxMs - minMs) + minMs : minMs;
return new Promise(resolve => {
setTimeout(() => resolve(value), delay);
});
}
// ๐ฏ Create a cancellable promise
static cancellable<T>(promise: Promise<T>): {
promise: Promise<T>;
cancel: () => void;
} {
let isCancelled = false;
let rejectFn: (reason?: any) => void;
const cancellablePromise = new Promise<T>((resolve, reject) => {
rejectFn = reject;
promise
.then(value => {
if (!isCancelled) {
resolve(value);
}
})
.catch(error => {
if (!isCancelled) {
reject(error);
}
});
});
return {
promise: cancellablePromise,
cancel: () => {
isCancelled = true;
rejectFn(new Error('Promise was cancelled ๐ซ'));
}
};
}
}
// ๐ฎ Example usage of advanced utilities
async function demonstrateAdvancedPatterns(): Promise<void> {
try {
// โฐ Timeout example
const quickData = await PromiseUtils.withTimeout(
fetchDataWithDelay(500),
1000,
"Data fetch took too long! โฐ"
);
console.log('โก Quick data:', quickData);
// ๐ Retry example
const retryData = await PromiseUtils.retry(
() => unreliableApiCall(),
{
maxAttempts: 3,
baseDelay: 500,
shouldRetry: (error) => !error.message.includes('validation')
}
);
console.log('๐ Retry data:', retryData);
// ๐๏ธ Parallel execution
const parallelTasks = Array.from({ length: 10 }, (_, i) =>
() => PromiseUtils.delay(`Task ${i + 1}`, 100, 1000)
);
const parallelResults = await PromiseUtils.parallel(parallelTasks, 3);
console.log('๐๏ธ Parallel results:', parallelResults);
} catch (error) {
console.error('โ Advanced patterns demo failed:', error);
}
}
// ๐ญ Mock functions for demonstration
async function fetchDataWithDelay(delayMs: number): Promise<string> {
await new Promise(resolve => setTimeout(resolve, delayMs));
return `Data fetched after ${delayMs}ms delay ๐`;
}
async function unreliableApiCall(): Promise<string> {
if (Math.random() < 0.7) {
throw new Error('Network error ๐');
}
return 'API call successful! โ
';
}
๐๏ธ Promise-Based State Management
Creating a type-safe state management system with promises:
// ๐ State management types
interface StateChange<T> {
previous: T;
current: T;
timestamp: Date;
action: string;
}
interface StateSubscription<T> {
id: string;
callback: (change: StateChange<T>) => void;
filter?: (change: StateChange<T>) => boolean;
}
// ๐ฏ Type-safe promise-based state manager
class PromiseStateManager<T> {
private state: T;
private subscriptions: Map<string, StateSubscription<T>> = new Map();
private pendingUpdates: Map<string, Promise<T>> = new Map();
private history: StateChange<T>[] = [];
private maxHistorySize: number = 100;
constructor(initialState: T) {
this.state = initialState;
console.log('๐ฏ State manager initialized');
}
// ๐ Get current state
getState(): T {
return { ...this.state } as T; // Return a copy to prevent mutations
}
// ๐ Update state with a promise-based updater
async updateState(
updater: (currentState: T) => Promise<T> | T,
actionName: string = 'UPDATE'
): Promise<T> {
const updateId = `${actionName}_${Date.now()}_${Math.random()}`;
// ๐ Prevent concurrent updates of the same action
if (this.pendingUpdates.has(actionName)) {
console.log(`โณ Waiting for pending ${actionName} to complete...`);
await this.pendingUpdates.get(actionName);
}
try {
console.log(`๐ Starting state update: ${actionName}`);
const updatePromise = this.performUpdate(updater, actionName);
this.pendingUpdates.set(actionName, updatePromise);
const newState = await updatePromise;
return newState;
} finally {
this.pendingUpdates.delete(actionName);
}
}
// ๐ญ Perform the actual state update
private async performUpdate(
updater: (currentState: T) => Promise<T> | T,
actionName: string
): Promise<T> {
const previousState = this.getState();
try {
// ๐ Apply the updater function
const result = updater(previousState);
const newState = await Promise.resolve(result);
// ๐ Record the state change
const change: StateChange<T> = {
previous: previousState,
current: newState,
timestamp: new Date(),
action: actionName
};
// ๐พ Update internal state
this.state = newState;
this.addToHistory(change);
// ๐ข Notify subscribers
await this.notifySubscribers(change);
console.log(`โ
State updated: ${actionName}`);
return newState;
} catch (error) {
console.error(`โ State update failed: ${actionName}`, error);
throw error;
}
}
// ๐ Add change to history
private addToHistory(change: StateChange<T>): void {
this.history.push(change);
// ๐งน Keep history size manageable
if (this.history.length > this.maxHistorySize) {
this.history = this.history.slice(-this.maxHistorySize);
}
}
// ๐ข Notify all subscribers about state changes
private async notifySubscribers(change: StateChange<T>): Promise<void> {
const notifications = Array.from(this.subscriptions.values())
.filter(sub => !sub.filter || sub.filter(change))
.map(async (sub) => {
try {
await Promise.resolve(sub.callback(change));
} catch (error) {
console.error(`โ Subscriber ${sub.id} error:`, error);
}
});
await Promise.all(notifications);
}
// ๐ Subscribe to state changes
subscribe(
callback: (change: StateChange<T>) => void,
filter?: (change: StateChange<T>) => boolean
): string {
const id = `sub_${Date.now()}_${Math.random()}`;
this.subscriptions.set(id, {
id,
callback,
filter
});
console.log(`๐ Subscriber ${id} added`);
return id;
}
// ๐ Unsubscribe from state changes
unsubscribe(subscriptionId: string): boolean {
const removed = this.subscriptions.delete(subscriptionId);
if (removed) {
console.log(`๐ Subscriber ${subscriptionId} removed`);
}
return removed;
}
// ๐ Get state history
getHistory(limit?: number): StateChange<T>[] {
return limit ? this.history.slice(-limit) : [...this.history];
}
// โช Rollback to a previous state
async rollback(steps: number = 1): Promise<T> {
if (this.history.length < steps) {
throw new Error(`Cannot rollback ${steps} steps, only ${this.history.length} changes in history`);
}
const targetState = this.history[this.history.length - steps - 1]?.current || this.history[0]?.previous;
if (!targetState) {
throw new Error('No target state found for rollback');
}
return this.updateState(() => targetState, `ROLLBACK_${steps}`);
}
}
// ๐ฎ Example usage with a shopping cart
interface CartItem {
id: string;
name: string;
price: number;
quantity: number;
}
interface ShoppingCartState {
items: CartItem[];
total: number;
discountCode?: string;
discountAmount: number;
}
// ๐ Shopping cart with promise-based state management
class ShoppingCart {
private stateManager: PromiseStateManager<ShoppingCartState>;
constructor() {
const initialState: ShoppingCartState = {
items: [],
total: 0,
discountAmount: 0
};
this.stateManager = new PromiseStateManager(initialState);
// ๐ Subscribe to total changes
this.stateManager.subscribe(
(change) => {
console.log(`๐ฐ Cart total: $${change.current.total.toFixed(2)}`);
},
(change) => change.previous.total !== change.current.total
);
}
// โ Add item to cart with async validation
async addItem(item: Omit<CartItem, 'id'>): Promise<ShoppingCartState> {
return this.stateManager.updateState(async (state) => {
// ๐ Simulate async inventory check
await this.checkInventory(item.name);
const newItem: CartItem = {
...item,
id: `item_${Date.now()}_${Math.random()}`
};
const existingItemIndex = state.items.findIndex(i => i.name === item.name);
let newItems: CartItem[];
if (existingItemIndex >= 0) {
// ๐ Update quantity of existing item
newItems = [...state.items];
newItems[existingItemIndex] = {
...newItems[existingItemIndex],
quantity: newItems[existingItemIndex].quantity + item.quantity
};
} else {
// โ Add new item
newItems = [...state.items, newItem];
}
const newTotal = this.calculateTotal(newItems, state.discountAmount);
return {
...state,
items: newItems,
total: newTotal
};
}, 'ADD_ITEM');
}
// ๐ท๏ธ Apply discount code with async validation
async applyDiscountCode(code: string): Promise<ShoppingCartState> {
return this.stateManager.updateState(async (state) => {
// ๐ Simulate async discount validation
const discount = await this.validateDiscountCode(code);
const discountAmount = state.total * discount.percentage;
const newTotal = this.calculateTotal(state.items, discountAmount);
return {
...state,
discountCode: code,
discountAmount,
total: newTotal
};
}, 'APPLY_DISCOUNT');
}
// ๐งฎ Calculate total
private calculateTotal(items: CartItem[], discountAmount: number = 0): number {
const subtotal = items.reduce((sum, item) => sum + (item.price * item.quantity), 0);
return Math.max(0, subtotal - discountAmount);
}
// ๐ Mock inventory check
private async checkInventory(itemName: string): Promise<void> {
await new Promise(resolve => setTimeout(resolve, 200));
if (itemName.toLowerCase().includes('outofstock')) {
throw new Error(`${itemName} is out of stock! ๐ฆ`);
}
}
// ๐ท๏ธ Mock discount validation
private async validateDiscountCode(code: string): Promise<{ percentage: number }> {
await new Promise(resolve => setTimeout(resolve, 300));
const validCodes: Record<string, number> = {
'SAVE10': 0.10,
'SAVE20': 0.20,
'WELCOME': 0.15
};
if (!validCodes[code]) {
throw new Error(`Invalid discount code: ${code} ๐ท๏ธ`);
}
return { percentage: validCodes[code] };
}
// ๐ Get current cart state
getState(): ShoppingCartState {
return this.stateManager.getState();
}
// ๐ Subscribe to cart changes
onCartChange(callback: (change: StateChange<ShoppingCartState>) => void): string {
return this.stateManager.subscribe(callback);
}
}
โ ๏ธ Common Pitfalls and Solutions
๐ฑ Pitfall 1: Losing Type Information in Promise Chains
// โ Wrong way - losing types in complex chains
async function fetchUserDataBad(): Promise<any> {
const response = await fetch('/api/user');
const data = await response.json(); // ๐ฅ Returns 'any'
const processed = await processData(data); // ๐ฅ Still 'any'
return processed; // ๐ฅ No type safety!
}
// Using the result loses all type information
const userData = await fetchUserDataBad();
// userData. <-- No autocomplete, no type safety
// โ
Correct way - maintain types throughout the chain
interface UserApiResponse {
user: {
id: string;
name: string;
email: string;
};
metadata: {
lastLogin: string;
preferences: Record<string, any>;
};
}
interface ProcessedUserData {
id: string;
name: string;
email: string;
lastLogin: Date;
preferences: Record<string, any>;
}
async function fetchUserDataGood(): Promise<ProcessedUserData> {
// ๐ฏ Type the response explicitly
const response = await fetch('/api/user');
const data: UserApiResponse = await response.json();
// ๐ Type-safe processing
const processed: ProcessedUserData = await processUserData(data);
return processed;
}
async function processUserData(data: UserApiResponse): Promise<ProcessedUserData> {
return {
id: data.user.id,
name: data.user.name,
email: data.user.email,
lastLogin: new Date(data.metadata.lastLogin),
preferences: data.metadata.preferences
};
}
// โ
Now we have full type safety!
const userData = await fetchUserDataGood();
console.log(userData.name); // ๐ฏ Full autocomplete and type checking
๐คฏ Pitfall 2: Not Handling Promise Rejection Types
// โ Wrong way - not typing errors properly
async function riskyOperation(): Promise<string> {
try {
const result = await someAsyncOperation();
return result;
} catch (error) {
// ๐ฅ 'error' is unknown type - no type safety!
console.log(error.message); // ๐ซ TypeScript error
throw error;
}
}
// โ
Correct way - proper error typing
interface ApiError {
code: string;
message: string;
statusCode: number;
}
class NetworkError extends Error {
constructor(public statusCode: number, message: string) {
super(message);
this.name = 'NetworkError';
}
}
async function safeOperation(): Promise<string> {
try {
const result = await someAsyncOperation();
return result;
} catch (error) {
// ๐ฏ Type-safe error handling
if (error instanceof NetworkError) {
console.log(`Network error ${error.statusCode}: ${error.message}`);
throw error;
} else if (error && typeof error === 'object' && 'code' in error) {
const apiError = error as ApiError;
console.log(`API error ${apiError.code}: ${apiError.message}`);
throw new Error(`API Error: ${apiError.message}`);
} else {
console.log(`Unknown error: ${String(error)}`);
throw new Error(`Unknown error: ${String(error)}`);
}
}
}
// ๐ก๏ธ Even better - use Result types for safer error handling
type OperationResult<T> =
| { success: true; data: T }
| { success: false; error: string; errorType: 'network' | 'api' | 'unknown' };
async function safestOperation(): Promise<OperationResult<string>> {
try {
const result = await someAsyncOperation();
return { success: true, data: result };
} catch (error) {
if (error instanceof NetworkError) {
return {
success: false,
error: error.message,
errorType: 'network'
};
} else {
return {
success: false,
error: String(error),
errorType: 'unknown'
};
}
}
}
๐ฅ Pitfall 3: Promise Constructor Anti-patterns
// โ Wrong way - unnecessary Promise constructor
async function unnecessaryPromiseConstructor(): Promise<string> {
return new Promise(async (resolve, reject) => {
try {
const data = await fetch('/api/data');
const result = await data.json();
resolve(result); // ๐ฅ This is redundant!
} catch (error) {
reject(error); // ๐ฅ This is also redundant!
}
});
}
// โ Also wrong - mixing callbacks with async/await
function mixedPatterns(): Promise<string> {
return new Promise((resolve, reject) => {
fetch('/api/data')
.then(async response => {
const data = await response.json(); // ๐ฅ Mixing patterns
resolve(data);
})
.catch(reject);
});
}
// โ
Correct way - use async/await directly
async function cleanAsyncFunction(): Promise<string> {
const response = await fetch('/api/data');
const data = await response.json();
return data; // โ
Clean and simple!
}
// โ
Use Promise constructor only when wrapping callbacks
function wrapCallbackAPI(): Promise<string> {
return new Promise((resolve, reject) => {
// ๐ฏ Only use Promise constructor for callback APIs
oldCallbackFunction((error: Error | null, result: string) => {
if (error) {
reject(error);
} else {
resolve(result);
}
});
});
}
// ๐ฏ Helper function to promisify callback APIs
function promisify<T>(
fn: (callback: (error: Error | null, result: T) => void) => void
): Promise<T> {
return new Promise((resolve, reject) => {
fn((error, result) => {
if (error) {
reject(error);
} else {
resolve(result);
}
});
});
}
// โ
Clean usage of promisify
const promisifiedFunction = () => promisify<string>(oldCallbackFunction);
๐ ๏ธ Best Practices
- ๐ฏ Always Type Promise Results: Use
Promise<T>
with specific types - ๐ Handle Errors Explicitly: Donโt let errors be
unknown
type - ๐ก๏ธ Use Result Types: Consider
Result<T, E>
pattern for safer error handling - ๐จ Avoid Promise Constructor: Use async/await instead of
new Promise
- โจ Compose Small Functions: Build complex async flows from simple parts
- ๐ Use Cancellation: Implement cancellation for long-running operations
- ๐ฆ Batch Related Operations: Use Promise.all for independent parallel tasks
๐งช Hands-On Exercise
๐ฏ Challenge: Build a Type-Safe Data Synchronization System
Create a robust data sync system that handles multiple data sources:
๐ Requirements:
- ๐ Sync data between local storage, remote API, and cache
- ๐ก๏ธ Full type safety for all data operations
- โ ๏ธ Comprehensive error handling with typed errors
- ๐ Progress tracking and status reporting
- ๐ฏ Conflict resolution for concurrent updates
- ๐ Retry logic with exponential backoff
- ๐จ Real-time status updates with promises!
๐ Bonus Points:
- Add offline detection and queue management
- Implement data versioning and merge strategies
- Create performance metrics and monitoring
๐ก Solution
๐ Click to see solution
// ๐ฏ Data synchronization system with full type safety!
// ๐ Core sync types
interface SyncableData {
id: string;
version: number;
updatedAt: Date;
data: Record<string, any>;
}
interface SyncStatus {
status: 'idle' | 'syncing' | 'error' | 'completed';
progress: number;
message: string;
lastSync?: Date;
errors: SyncError[];
}
interface SyncError {
source: 'local' | 'remote' | 'cache';
operation: 'read' | 'write' | 'delete';
error: string;
timestamp: Date;
dataId?: string;
}
interface SyncResult<T extends SyncableData> {
success: boolean;
syncedItems: T[];
conflicts: ConflictResolution<T>[];
errors: SyncError[];
duration: number;
}
interface ConflictResolution<T extends SyncableData> {
localData: T;
remoteData: T;
resolution: 'local' | 'remote' | 'merged';
resolvedData: T;
}
// ๐๏ธ Type-safe data synchronizer
class DataSynchronizer<T extends SyncableData> {
private syncStatus: SyncStatus = {
status: 'idle',
progress: 0,
message: 'Ready to sync',
errors: []
};
private statusSubscribers: ((status: SyncStatus) => void)[] = [];
constructor(
private localStore: DataStore<T>,
private remoteStore: DataStore<T>,
private cacheStore: DataStore<T>
) {}
// ๐ Main synchronization method
async synchronize(): Promise<SyncResult<T>> {
const startTime = Date.now();
try {
this.updateStatus('syncing', 0, 'Starting synchronization...');
// ๐ Phase 1: Fetch data from all sources
const [localData, remoteData, cacheData] = await Promise.all([
this.safeOperation(() => this.localStore.getAll(), 'local', 'read'),
this.safeOperation(() => this.remoteStore.getAll(), 'remote', 'read'),
this.safeOperation(() => this.cacheStore.getAll(), 'cache', 'read')
]);
this.updateStatus('syncing', 25, 'Data fetched, analyzing differences...');
// ๐ Phase 2: Detect conflicts and differences
const conflicts = this.detectConflicts(localData, remoteData);
const resolvedConflicts = await this.resolveConflicts(conflicts);
this.updateStatus('syncing', 50, 'Conflicts resolved, syncing changes...');
// ๐ Phase 3: Apply resolved changes
const syncedItems = await this.applySyncChanges(resolvedConflicts, localData, remoteData);
this.updateStatus('syncing', 75, 'Updating cache...');
// ๐พ Phase 4: Update cache with synced data
await this.updateCache(syncedItems);
this.updateStatus('completed', 100, 'Synchronization completed successfully!');
const result: SyncResult<T> = {
success: true,
syncedItems,
conflicts: resolvedConflicts,
errors: this.syncStatus.errors,
duration: Date.now() - startTime
};
console.log(`โ
Sync completed in ${result.duration}ms`);
return result;
} catch (error) {
this.updateStatus('error', this.syncStatus.progress, `Sync failed: ${error}`);
return {
success: false,
syncedItems: [],
conflicts: [],
errors: this.syncStatus.errors,
duration: Date.now() - startTime
};
}
}
// ๐ก๏ธ Safe operation wrapper
private async safeOperation<R>(
operation: () => Promise<R>,
source: SyncError['source'],
operationType: SyncError['operation']
): Promise<R> {
try {
return await PromiseUtils.withTimeout(operation(), 5000);
} catch (error) {
const syncError: SyncError = {
source,
operation: operationType,
error: error instanceof Error ? error.message : String(error),
timestamp: new Date()
};
this.syncStatus.errors.push(syncError);
console.error(`โ ${source} ${operationType} failed:`, error);
throw error;
}
}
// ๐ Detect conflicts between local and remote data
private detectConflicts(localData: T[], remoteData: T[]): ConflictResolution<T>[] {
const conflicts: ConflictResolution<T>[] = [];
const remoteMap = new Map(remoteData.map(item => [item.id, item]));
for (const localItem of localData) {
const remoteItem = remoteMap.get(localItem.id);
if (remoteItem && localItem.version !== remoteItem.version) {
// ๐ฅ Version conflict detected
conflicts.push({
localData: localItem,
remoteData: remoteItem,
resolution: 'local', // Default, will be resolved
resolvedData: localItem // Temporary
});
}
}
console.log(`๐ Detected ${conflicts.length} conflicts`);
return conflicts;
}
// ๐ค Resolve conflicts using various strategies
private async resolveConflicts(conflicts: ConflictResolution<T>[]): Promise<ConflictResolution<T>[]> {
const resolved: ConflictResolution<T>[] = [];
for (const conflict of conflicts) {
const resolution = await this.resolveConflict(conflict);
resolved.push(resolution);
}
return resolved;
}
// ๐ฏ Individual conflict resolution
private async resolveConflict(conflict: ConflictResolution<T>): Promise<ConflictResolution<T>> {
const { localData, remoteData } = conflict;
// ๐
Timestamp-based resolution (latest wins)
if (localData.updatedAt > remoteData.updatedAt) {
return {
...conflict,
resolution: 'local',
resolvedData: { ...localData, version: Math.max(localData.version, remoteData.version) + 1 }
};
} else if (remoteData.updatedAt > localData.updatedAt) {
return {
...conflict,
resolution: 'remote',
resolvedData: { ...remoteData, version: Math.max(localData.version, remoteData.version) + 1 }
};
} else {
// ๐ Same timestamp - merge data
const mergedData: T = {
...localData,
...remoteData,
id: localData.id,
version: Math.max(localData.version, remoteData.version) + 1,
updatedAt: new Date(),
data: { ...localData.data, ...remoteData.data }
};
return {
...conflict,
resolution: 'merged',
resolvedData: mergedData
};
}
}
// ๐ Apply sync changes to stores
private async applySyncChanges(
resolvedConflicts: ConflictResolution<T>[],
localData: T[],
remoteData: T[]
): Promise<T[]> {
const syncedItems: T[] = [];
// ๐ Apply conflict resolutions
for (const conflict of resolvedConflicts) {
const { resolvedData, resolution } = conflict;
try {
// ๐พ Update both stores with resolved data
await Promise.all([
this.localStore.update(resolvedData),
this.remoteStore.update(resolvedData)
]);
syncedItems.push(resolvedData);
console.log(`๐ Resolved conflict for ${resolvedData.id} using ${resolution} strategy`);
} catch (error) {
console.error(`โ Failed to apply conflict resolution for ${resolvedData.id}:`, error);
}
}
return syncedItems;
}
// ๐พ Update cache with synced data
private async updateCache(syncedItems: T[]): Promise<void> {
try {
await Promise.all(
syncedItems.map(item => this.cacheStore.update(item))
);
console.log(`๐พ Updated cache with ${syncedItems.length} items`);
} catch (error) {
console.error('โ Cache update failed:', error);
}
}
// ๐ Update sync status and notify subscribers
private updateStatus(status: SyncStatus['status'], progress: number, message: string): void {
this.syncStatus = {
...this.syncStatus,
status,
progress,
message,
lastSync: status === 'completed' ? new Date() : this.syncStatus.lastSync
};
console.log(`๐ Sync ${status}: ${progress}% - ${message}`);
this.statusSubscribers.forEach(callback => callback(this.syncStatus));
}
// ๐ Subscribe to status updates
onStatusUpdate(callback: (status: SyncStatus) => void): () => void {
this.statusSubscribers.push(callback);
return () => {
const index = this.statusSubscribers.indexOf(callback);
if (index > -1) {
this.statusSubscribers.splice(index, 1);
}
};
}
// ๐ Get current sync status
getStatus(): SyncStatus {
return { ...this.syncStatus };
}
}
// ๐ฆ Mock data store interface
interface DataStore<T extends SyncableData> {
getAll(): Promise<T[]>;
update(item: T): Promise<void>;
delete(id: string): Promise<void>;
}
// ๐ญ Mock implementation for demonstration
class MockDataStore<T extends SyncableData> implements DataStore<T> {
private data: Map<string, T> = new Map();
constructor(private storeName: string, initialData: T[] = []) {
initialData.forEach(item => this.data.set(item.id, item));
}
async getAll(): Promise<T[]> {
await PromiseUtils.delay(null, 100, 300); // Simulate network delay
return Array.from(this.data.values());
}
async update(item: T): Promise<void> {
await PromiseUtils.delay(null, 50, 150);
this.data.set(item.id, item);
console.log(`๐ฆ ${this.storeName}: Updated ${item.id}`);
}
async delete(id: string): Promise<void> {
await PromiseUtils.delay(null, 50, 100);
this.data.delete(id);
console.log(`๐๏ธ ${this.storeName}: Deleted ${id}`);
}
}
// ๐ฎ Example usage
interface UserData extends SyncableData {
data: {
name: string;
email: string;
preferences: Record<string, any>;
};
}
async function demonstrateDataSync(): Promise<void> {
// ๐ฆ Create mock data stores
const localStore = new MockDataStore<UserData>('Local', [
{
id: 'user1',
version: 1,
updatedAt: new Date(Date.now() - 60000), // 1 minute ago
data: { name: 'Alice', email: '[email protected]', preferences: { theme: 'dark' } }
}
]);
const remoteStore = new MockDataStore<UserData>('Remote', [
{
id: 'user1',
version: 2,
updatedAt: new Date(), // Now
data: { name: 'Alice Updated', email: '[email protected]', preferences: { theme: 'light' } }
}
]);
const cacheStore = new MockDataStore<UserData>('Cache');
// ๐ Create synchronizer
const synchronizer = new DataSynchronizer(localStore, remoteStore, cacheStore);
// ๐ Subscribe to status updates
const unsubscribe = synchronizer.onStatusUpdate((status) => {
console.log(`๐ Status: ${status.status} (${status.progress}%) - ${status.message}`);
});
try {
// ๐ Perform synchronization
const result = await synchronizer.synchronize();
console.log('\n๐ Synchronization Result:');
console.log(`โ
Success: ${result.success}`);
console.log(`๐ Synced items: ${result.syncedItems.length}`);
console.log(`๐ฅ Conflicts: ${result.conflicts.length}`);
console.log(`โ Errors: ${result.errors.length}`);
console.log(`โฑ๏ธ Duration: ${result.duration}ms`);
} finally {
unsubscribe();
}
}
๐ Key Takeaways
Youโve mastered type-safe asynchronous programming in TypeScript! Hereโs what you can now do:
- โ Create bulletproof Promise types with complete type safety ๐ช
- โ Handle async errors gracefully with typed error handling ๐ก๏ธ
- โ Build complex async workflows that are impossible to break ๐ฏ
- โ Compose promises efficiently with advanced patterns and utilities ๐ง
- โ Debug async code confidently with full type information ๐
Remember: Type-safe promises transform unreliable async operations into predictable, maintainable systems. Your future self will thank you! ๐ค
๐ค Next Steps
Congratulations! ๐ Youโve mastered Promises in TypeScript!
Hereโs what to do next:
- ๐ป Practice with the data synchronization exercise above
- ๐๏ธ Refactor existing async code to use proper Promise types
- ๐ Move on to our next tutorial: Jest with TypeScript: Test Runner Setup
- ๐ Share your type-safe async patterns with the community!
Remember: Every typed promise is a step toward bulletproof applications. Keep building the future! ๐
Happy async coding! ๐โกโจ