Typing cho Promise và Async/Await
Bài 18 – Cách gán kiểu đúng cho Promise và Async/Await trong TypeScript theo cách đơn giản, trực quan và áp dụng ngay.
Typing cho Promise và Async/Await
Promise và async/await là nền tảng của lập trình bất đồng bộ trong JavaScript. Khi kết hợp TypeScript, bạn có thể xác định rõ dữ liệu mà Promise trả về, tránh lỗi khó đoán và tăng độ an toàn khi xử lý API.
2. Nội dung chính
- Promise là gì và typing cơ bản
- Gán kiểu cho Promise trả về
- Async/await với typing chính xác
- Typing cho hàm bất đồng bộ có error
- Promise.all – Promise.race – cách typing đúng
- Best practices khi typing bất đồng bộ
3. Ví dụ chi tiết
3.1. Promise có kiểu trả về
// Basic Promise typing
function fetchNumber(): Promise<number> {
return Promise.resolve(42);
}
function fetchString(): Promise<string> {
return Promise.resolve("Hello TypeScript");
}
function fetchBoolean(): Promise<boolean> {
return Promise.resolve(true);
}
// Usage
fetchNumber().then((n) => {
console.log(n); // n is number
return n * 2;
}).then((doubled) => {
console.log(doubled); // doubled is number
});
3.2. Async/await tự động suy luận kiểu trả về
// TypeScript automatically infers return type
async function fetchUser() {
return { id: 1, name: "Alice", age: 25 };
}
async function fetchProduct() {
return { id: 1, title: "Laptop", price: 999 };
}
// Usage
async function demo() {
const user = await fetchUser(); // {id: number, name: string, age: number}
const product = await fetchProduct(); // {id: number, title: string, price: number}
console.log(user.name); // "Alice"
console.log(product.title); // "Laptop"
}
3.3. Async function với kiểu rõ ràng
// Explicit typing for async functions
interface User {
id: number;
name: string;
email: string;
role: "admin" | "user";
}
interface Product {
id: number;
title: string;
price: number;
inStock: boolean;
}
async function fetchUser(id: number): Promise<User> {
// Simulate API call
return {
id,
name: "John Doe",
email: "john@example.com",
role: "user"
};
}
async function fetchProduct(id: number): Promise<Product> {
return {
id,
title: "Keyboard",
price: 49.99,
inStock: true
};
}
// Usage
async function loadData() {
const user = await fetchUser(1);
const product = await fetchProduct(1);
console.log(`${user.name} - ${user.role}`);
console.log(`${product.title} - $${product.price}`);
}
3.4. Typing lỗi trong async function
// Error types
interface ApiError {
message: string;
code: number;
status?: string;
}
interface NetworkError {
type: "network";
message: string;
}
interface ValidationError {
type: "validation";
message: string;
fields: Record<string, string[]>;
}
type AppError = ApiError | NetworkError | ValidationError;
// Async function with error handling
type Result<T, E = AppError> =
| { success: true; data: T }
| { success: false; error: E };
async function loadData(): Promise<Result<string, ApiError>> {
try {
// Simulate API call
const response = await fetch("/api/data");
if (!response.ok) {
return {
success: false,
error: {
message: "API Error",
code: response.status,
status: response.statusText
}
};
}
const data = await response.text();
return {
success: true,
data
};
} catch (error) {
return {
success: false,
error: {
message: "Network error",
code: 500,
status: "Failed to fetch"
}
};
}
}
// Usage with proper error handling
async function handleLoadData() {
const result = await loadData();
if (result.success) {
console.log("Data loaded:", result.data);
} else {
console.error("Error:", result.error.message);
}
}
3.5. Promise.all typing
// Multiple promises with different types
const p1 = Promise.resolve(42); // Promise<number>
const p2 = Promise.resolve("hello"); // Promise<string>
const p3 = Promise.resolve(true); // Promise<boolean>
const p4 = Promise.resolve({ id: 1, name: "Alice" }); // Promise<{id: number, name: string}>
// Promise.all creates tuple type
const result = await Promise.all([p1, p2, p3, p4]);
// result: [number, string, boolean, {id: number, name: string}]
console.log(result[0]); // number: 42
console.log(result[1]); // string: "hello"
console.log(result[2]); // boolean: true
console.log(result[3]); // object: {id: 1, name: "Alice"}
// Real-world example
async function loadMultipleUsers(userIds: number[]): Promise<User[]> {
const userPromises = userIds.map(id => fetchUser(id));
const users = await Promise.all(userPromises);
return users;
}
// Usage
async function demoMultipleUsers() {
const users = await loadMultipleUsers([1, 2, 3]);
users.forEach(user => {
console.log(`${user.id}: ${user.name}`);
});
}
3.6. Promise.race và Promise.allSettled typing
// Promise.race - first settled promise
async function raceExample() {
const fast = Promise.resolve("fast");
const slow = new Promise<string>(resolve =>
setTimeout(() => resolve("slow"), 1000)
);
const winner = await Promise.race([fast, slow]);
console.log(winner); // "fast" - string type
}
// Promise.allSettled - all results regardless of success/failure
interface User {
id: number;
name: string;
}
interface Product {
id: number;
title: string;
}
async function allSettledExample() {
const promises = [
Promise.resolve({ id: 1, name: "Alice" } as User),
Promise.reject(new Error("Product not found")),
Promise.resolve({ id: 1, title: "Book" } as Product)
];
const results = await Promise.allSettled(promises);
results.forEach((result, index) => {
if (result.status === "fulfilled") {
console.log(`Promise ${index} succeeded:`, result.value);
} else {
console.log(`Promise ${index} failed:`, result.reason);
}
});
}
3.7. Hàm bất đồng bộ generic
// Generic async function
async function fetchData<T>(url: string): Promise<T> {
const response = await fetch(url);
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
return response.json() as Promise<T>;
}
// Usage with different types
interface User {
id: number;
name: string;
email: string;
}
interface Product {
id: number;
title: string;
price: number;
}
interface ApiResponse<T> {
data: T;
status: string;
message?: string;
}
// Type-safe API calls
async function loadUserData() {
const user = await fetchData<User>("/api/users/1");
console.log(user.name); // TypeScript knows user has name property
}
async function loadProductData() {
const product = await fetchData<Product>("/api/products/1");
console.log(product.title); // TypeScript knows product has title property
}
async function loadApiResponse() {
const response = await fetchData<ApiResponse<User>>("/api/users/1");
console.log(response.data.name); // TypeScript knows response.data has name property
}
3.8. Retry logic với typing
// Retry function with proper typing
async function retry<T>(
fn: () => Promise<T>,
maxRetries: number = 3,
delay: number = 1000
): Promise<T> {
let lastError: Error;
for (let i = 0; i < maxRetries; i++) {
try {
return await fn();
} catch (error) {
lastError = error instanceof Error ? error : new Error(String(error));
if (i < maxRetries - 1) {
await new Promise(resolve => setTimeout(resolve, delay));
}
}
}
throw lastError!;
}
// Usage
async function fetchWithRetry<T>(url: string): Promise<T> {
return retry(async () => {
const response = await fetch(url);
if (!response.ok) {
throw new Error(`HTTP ${response.status}`);
}
return response.json();
}, 3, 1000);
}
// Example usage
async function demoRetry() {
try {
const user = await fetchWithRetry<User>("/api/users/1");
console.log("User loaded:", user.name);
} catch (error) {
console.error("Failed after 3 retries:", error);
}
}
3. Kiến thức trọng tâm
3.2. Promise luôn phải chỉ rõ kiểu T
- Giúp bạn biết dữ liệu nhận được là gì mà không cần đoán
- Type-safe error handling với proper error types
- Generic constraints cho flexible typing
3.3. Async function luôn trả về Promise
- Dù bạn return kiểu gì → TypeScript tự wrap vào Promise
- return type inference hoạt động tự động
- Error propagation trong async/await chains
3.4. Promise.all tạo ra tuple tương ứng với từng Promise con
- Đây là cách typing rất mạnh khi xử lý nhiều API song song
- Maintains type information cho mỗi promise
- Enables parallel processing với type safety
💡 GHI NHỚ: Async/await typing là foundation cho modern JavaScript applications
4. Bài tập thực h� nh
Bài ’: Viết hàm getToken() và getProfile()
// '. Viết hàm getToken(): Promise<string>
async function getToken(): Promise<string> {
// Simulate API call
return new Promise((resolve) => {
setTimeout(() => resolve("eyJhbGciOiJIUzI2NiIsInR5cCI6IkpXVCJ9"), 1000);
});
}
// '. Viết hàm async getProfile(): Promise<{id:number; name:string}>
interface Profile {
id: number;
name: string;
email: string;
avatar?: string;
}
async function getProfile(): Promise<Profile> {
return {
id: ',
name: "John Doe",
email: "john@example.com",
avatar: "https://example.com/avatar.jpg"
};
}
// 3. Sử dụng
async function demoAuth() {
try {
const token = await getToken();
console.log("Token received:", token.substring('', '') + "...");
const profile = await getProfile();
console.log(`Profile: ${profile.name} (${profile.email})`);
} catch (error) {
console.error("Auth error:", error);
}
}
Bài ’: Tạo hàm generic request()
// 1. Tạo hàm generic request<T>(url:string): Promise<T>
interface ApiResponse<T> {
data: T;
status: "success" | "error";
message?: string;
}
async function request<T>(url: string): Promise<ApiResponse<T>> {
try {
const response = await fetch(url);
if (!response.ok) {
return {
data: null as any,
status: "error",
message: `HTTP ${response.status}: ${response.statusText}`
};
}
const data = await response.json();
return {
data,
status: "success"
};
} catch (error) {
return {
data: null as any,
status: "error",
message: error instanceof Error ? error.message : "Unknown error"
};
}
}
// 2. Dùng để fetch user
interface User {
id: number;
name: string;
email: string;
role: "admin" | "user";
}
async function fetchUser(id: number): Promise<void> {
const result = await request<User>(`/api/users/${id}`);
if (result.status === "success" && result.data) {
console.log(`User: ${result.data.name} (${result.data.email})`);
} else {
console.error("Failed to fetch user:", result.message);
}
}
// 3. Dùng cho danh sách users
interface Paginated<T> {
items: T[];
total: number;
page: number;
pageSize: number;
}
async function fetchUsers(): Promise<void> {
const result = await request<Paginated<User>>("/api/users");
if (result.status === "success" && result.data) {
console.log(`Total users: ${result.data.total}`);
result.data.items.forEach(user => {
console.log(`- ${user.name} (${user.role})`);
});
}
}
Bài 3: Tạo parallel requests với Promise.all
// 1. Tạo multiple API functions
interface User {
id: number;
name: string;
email: string;
}
interface Post {
id: number;
title: string;
content: string;
authorId: number;
}
interface Comment {
id: number;
text: string;
postId: number;
authorId: number;
}
async function fetchUser(id: number): Promise<User> {
return new Promise(resolve => {
setTimeout(() => resolve({
id,
name: "John Doe",
email: "john@example.com"
}), 500);
});
}
async function fetchUserPosts(userId: number): Promise<Post[]> {
return new Promise(resolve => {
setTimeout(() => resolve([
{ id: 1, title: "Post 1", content: "Content 1", authorId: userId },
{ id: 2, title: "Post 2", content: "Content 2", authorId: userId }
]), 700);
});
}
async function fetchPostComments(postId: number): Promise<Comment[]> {
return new Promise(resolve => {
setTimeout(() => resolve([
{ id: 1, text: "Great post!", postId, authorId: 2 },
{ id: 2, text: "Thanks for sharing", postId, authorId: 3 }
]), 300);
});
}
// 2. Tạo parallel loading function
async function loadUserDashboard(userId: number) {
console.log("Loading user dashboard...");
const startTime = Date.now();
// Load all data in parallel
const [user, posts] = await Promise.all([
fetchUser(userId),
fetchUserPosts(userId)
]);
// Load comments for all posts in parallel
const comments = await Promise.all(
posts.map(post => fetchPostComments(post.id))
);
const endTime = Date.now();
return {
user,
posts: posts.map((post, index) => ({
...post,
comments: comments[index]
})),
loadTime: endTime - startTime
};
}
// 3. Sử dụng
async function demoDashboard() {
try {
const dashboard = await loadUserDashboard(1);
console.log(`\nDashboard loaded in ${dashboard.loadTime}ms`);
console.log(`User: ${dashboard.user.name}`);
console.log(`Posts: ${dashboard.posts.length}`);
dashboard.posts.forEach(post => {
console.log(`\n- ${post.title} (${post.comments.length} comments)`);
post.comments.forEach(comment => {
console.log(` * ${comment.text}`);
});
});
} catch (error) {
console.error("Failed to load dashboard:", error);
}
}
// Run demo
demoDashboard();
5. Sai lầm thường gặp
- Không typing Promise return values - mất type safety
- Missing error handling - unhandled promise rejections
- Không dùng Promise.all cho parallel requests - performance issues
- Type assertions thay vì proper typing - runtime errors
- Không handle Timeout scenarios - hanging requests
⚠️ GHI NHỚ: Async/await error handling là critical cho production apps
6. Kết luận
Typing cho Promise và async/await giúp bạn viết code bất đồng bộ an toàn hơn, chính xác hơn và dễ bảo trì. Đây là kỹ năng thiết yếu cho mọi ứng dụng front-end hoặc back-end sử dụng TypeScript.
🔑 GHI NHỚ QUAN TRỌNG:
- Luôn typing Promise return values
- Handle errors properly với try-catch
- Use Promise.all cho parallel operations
- Implement retry logic cho critical operations
- Test async code với proper mocking