Type Assertion và Casting trong TypeScript
Bài 16 – Type Assertion và Casting trong TypeScript theo cách đơn giản, trực quan và áp dụng ngay.
Type Assertion và Casting trong TypeScript
Type Assertion và Casting là kỹ thuật cho phép bạn “ép kiểu” hoặc thông báo cho TypeScript biết kiểu dữ liệu chính xác của một biến. Đây là công cụ hữu ích khi làm việc với DOM, API response hoặc các tình huống mà TypeScript không thể tự động suy luận kiểu.
Nội dung chính
- Type assertion là gì và khi nào sử dụng
- Cú pháp type assertion:
asvà angle bracket<> - Type assertion vs Type casting - khác biệt quan trọng
- Làm việc với DOM và type assertion
- Type assertion với union types và unknown
- Các pattern thường gặp và best practices
Ví dụ chi tiết
1. Type assertion cơ bản
// TypeScript không biết element có id này tồn tại
const input = document.getElementById('username');
// ❌ Lỗi: Property 'value' does not exist on type 'HTMLElement'
// console.log(input.value);
// ✅ Sử dụng type assertion để chỉ rõ kiểu
const input = document.getElementById('username') as HTMLInputElement;
console.log(input.value); // Giờ hoạt động rồi!
2. Hai cú pháp type assertion
// Cách 1: Sử dụng từ khóa 'as' (khuyến nghị)
const input1 = document.getElementById('email') as HTMLInputElement;
// Cách 2: Sử dụng angle brackets (không khuyến nghị trong .tsx files)
const input2 = <HTMLInputElement>document.getElementById('email');
// Cả hai đều hoạt động giống nhau
console.log(input1.value);
console.log(input2.value);
3. Type assertion với union types
type StringOrNumber = string | number;
function processValue(value: StringOrNumber) {
// ❌ Không thể truy cập thuộc tính string-specific
// console.log(value.toUpperCase());
// ✅ Sử dụng type assertion để thu hẹp kiểu
if (typeof value === 'string') {
console.log((value as string).toUpperCase());
} else {
console.log((value as number).toFixed(2));
}
}
processValue("hello"); // HELLO
processValue(3.14159); // 3.14
4. Làm việc với API response
// Giả sử API trả về dữ liệu không rõ ràng
type User = {
id: number;
name: string;
email: string;
};
async function fetchUser(userId: number): Promise<User> {
const response = await fetch(`/api/users/${userId}`);
const data = await response.json();
// ❌ data có kiểu any, không có type safety
// return data;
// ✅ Sử dụng type assertion để chỉ định kiểu
return data as User;
}
// Sử dụng
const user = await fetchUser(1);
console.log(user.name); // Giờ có autocomplete và type checking!
5. Type assertion với HTMLElement
// Các trường hợp thường gặp với DOM
const canvas = document.getElementById('gameCanvas') as HTMLCanvasElement;
const ctx = canvas.getContext('2d'); // Giờ hoạt động!
const video = document.getElementById('videoPlayer') as HTMLVideoElement;
video.play(); // Có thể gọi play() method
const select = document.getElementById('country') as HTMLSelectElement;
console.log(select.selectedIndex); // Có thể truy cập selectedIndex
6. Chuyển đổi từ unknown sang concrete type
function processUnknown(value: unknown) {
// ❌ Không thể truy cập thuộc tính của unknown
// console.log(value.name);
// ✅ Kiểm tra kiểu trước rồi assert
if (typeof value === 'object' && value !== null) {
// Chuyển đổi sang object generic trước
const obj = value as Record<string, unknown>;
// Sau đó kiểm tra thuộc tính cụ thể
if (typeof obj.name === 'string') {
console.log(`Name: ${obj.name}`);
}
}
}
processUnknown({ name: "Alice", age: 30 });
7. Type assertion với optional chaining
interface Company {
name?: string;
address?: {
street?: string;
city?: string;
};
}
function getCompanyCity(company: Company): string {
// Kết hợp type assertion với optional chaining
return company.address?.city ?? "Unknown city";
}
// Hoặc sử dụng assertion để bypass optional checking
function getCompanyNameForce(company: Company): string {
// ❌ Có thể gây lỗi runtime nếu name undefined
// return (company as Required<Company>).name;
// ✅ An toàn hơn với fallback
return (company.name as string | undefined) ?? "Unknown company";
}
Kiến thức trọng tâm
1. Type assertion không thực sự “ép kiểu” dữ liệu
- Type assertion chỉ ảnh hưởng compile-time, không thay đổi runtime value
- Không có conversion thực sự - chỉ là gợi ý cho TypeScript
- Có thể gây lỗi runtime nếu assertion không chính xác
const value = "hello" as number; // ❌ Sai nhưng TypeScript không báo lỗi!
console.log(value.toFixed(2)); // Runtime error: toFixed is not a function
2. Khi nào nên sử dụng type assertion
- Làm việc với DOM: Khi TypeScript không biết kiểu chính xác của element
- API response: Khi bạn chắc chắn về cấu trúc dữ liệu từ API
- Migration từ JavaScript: Trong quá trình chuyển đổi sang TypeScript
- Interop với thư viện JavaScript: Khi sử dụng thư viện không có type definitions
3. Khi KHÔNG nên sử dụng type assertion
- Thay vì proper type checking: Luôn ưu tiên kiểm tra kiểu thực tế
- Để bypass compiler warnings: Đây thường là code smell
- Khi có thể định nghĩa types rõ ràng: Hãy định nghĩa interface thay vì assert
4. Type assertion vs Type guards
// ❌ Type assertion - bypass type checking
function process(value: string | number) {
return (value as string).toUpperCase(); // Có thể gây lỗi
}
// ✅ Type guards - an toàn hơn
function processSafe(value: string | number) {
if (typeof value === 'string') {
return value.toUpperCase(); // TypeScript hiểu đây là string
}
return value.toFixed(2); // TypeScript hiểu đây là number
}
Bài tập thực hành
Bài 1: DOM Element Type Assertion
// Lấy các element từ DOM và assert đúng kiểu
const emailInput = document.getElementById('email') as HTMLInputElement;
const submitButton = document.getElementById('submit') as HTMLButtonElement;
const errorDiv = document.getElementById('error') as HTMLDivElement;
function validateEmail(): boolean {
const email = emailInput.value;
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
if (!emailRegex.test(email)) {
errorDiv.textContent = "Email không hợp lệ";
errorDiv.style.display = "block";
return false;
}
errorDiv.style.display = "none";
return true;
}
submitButton.addEventListener('click', validateEmail);
Bài 2: API Response Type Assertion
interface Product {
id: number;
name: string;
price: number;
category: string;
}
interface ApiResponse {
success: boolean;
data: Product[];
message?: string;
}
async function fetchProducts(): Promise<Product[]> {
try {
const response = await fetch('/api/products');
const data = await response.json() as ApiResponse;
if (!data.success) {
throw new Error(data.message || "Failed to fetch products");
}
return data.data; // Giờ có thể truy cập products với type safety
} catch (error) {
console.error("Error fetching products:", error);
return [];
}
}
// Sử dụng
const products = await fetchProducts();
products.forEach(product => {
console.log(`${product.name}: $${product.price}`);
});
Bài 3: Type Assertion với Union Types
type Shape =
| { type: "circle"; radius: number }
| { type: "rectangle"; width: number; height: number }
| { type: "triangle"; base: number; height: number };
function calculateArea(shape: Shape): number {
switch (shape.type) {
case "circle":
// TypeScript tự động narrow type, không cần assertion
return Math.PI * shape.radius ** 2;
case "rectangle":
return shape.width * shape.height;
case "triangle":
return (shape.base * shape.height) / 2;
default:
// Exhaustive check với type assertion
const _exhaustiveCheck: never = shape;
throw new Error(`Unhandled shape type: ${_exhaustiveCheck}`);
}
}
// Sử dụng
const circle: Shape = { type: "circle", radius: 5 };
const rectangle: Shape = { type: "rectangle", width: 4, height: 6 };
console.log(`Circle area: ${calculateArea(circle)}`);
console.log(`Rectangle area: ${calculateArea(rectangle)}`);
Sai lầm thường gặp
1. Type assertion với sai kiểu dữ liệu
// ❌ Sai: Assert string thành number
const value = "hello" as number;
console.log(value.toFixed(2)); // Runtime error!
// ✅ Đúng: Kiểm tra kiểu trước khi assert
const input = document.getElementById('numberInput') as HTMLInputElement;
const num = parseFloat(input.value);
if (!isNaN(num)) {
console.log(num.toFixed(2));
}
2. Sử dụng type assertion thay vì type guards
// ❌ Bypass type safety
function processValue(value: string | number) {
return (value as string).toUpperCase(); // Có thể gây lỗi
}
// ✅ Sử dụng type guards
function processValueSafe(value: string | number) {
if (typeof value === 'string') {
return value.toUpperCase();
}
return value.toFixed(2);
}
3. Assert với any hoặc unknown mà không kiểm tra
// ❌ Quá mạo hiểm
function processAny(data: unknown) {
return (data as { name: string }).name; // Có thể gây lỗi runtime
}
// ✅ An toàn hơn
function processUnknownSafe(data: unknown) {
if (typeof data === 'object' && data !== null && 'name' in data) {
const obj = data as Record<string, unknown>;
if (typeof obj.name === 'string') {
return obj.name;
}
}
return "Unknown";
}
4. Assert với optional properties mà không kiểm tra
interface User {
name?: string;
email?: string;
}
// ❌ Có thể gây lỗi nếu property undefined
function getUserName(user: User): string {
return (user.name as string).toUpperCase(); // Có thể gây lỗi!
}
// ✅ An toàn hơn với optional chaining
function getUserNameSafe(user: User): string {
return user.name?.toUpperCase() ?? "Anonymous";
}
Kết luận
Type assertion là công cụ mạnh mẽ nhưng cần được sử dụng cẩn thận. Nó giúp bạn làm việc với các tình huống TypeScript không thể suy luận kiểu tự động, nhưng cũng có thể bypass type safety nếu dùng sai cách.
🔑 GHI NHỚ QUAN TRỌNG:
- Type assertion chỉ ảnh hưởng compile-time, không thay đổi runtime
- Luôn ưu tiên type guards và proper type checking
- Sử dụng assertion cho DOM, API response, và JS interop
- Tránh assert với kiểu không tương thích - có thể gây lỗi runtime
- Kết hợp với optional chaining và nullish coalescing để an toàn hơn