Bài 19.2: Typing route parameters

Bài 19.2: Typing route parameters
Trong thế giới phát triển web hiện đại, việc xây dựng các trang web động là vô cùng phổ biến. Thay vì tạo ra hàng nghìn trang tĩnh, chúng ta sử dụng route động (dynamic routes) để hiển thị các nội dung khác nhau dựa trên một phần của URL. Ví dụ, /products/123
, /users/john-doe
, hay /blog/typescript-guide
.
Phần 123
, john-doe
, hay typescript-guide
chính là tham số route (route parameters). Chúng mang thông tin cần thiết để component của bạn biết phải hiển thị dữ liệu gì.
Tuyệt vời, nhưng làm thế nào để chúng ta chắc chắn rằng khi lấy các tham số này ra từ URL, chúng ta biết chắc kiểu dữ liệu của nó là gì? Liệu productId
luôn là một chuỗi (string) có thể chuyển đổi thành số, hay nó có thể là undefined
? Việc lấy nhầm tên tham số, hoặc truy cập một tham số mà ta nghĩ là có nhưng thực tế lại không tồn tại trên route hiện tại, có thể dẫn đến lỗi runtime không ngờ tới, khiến ứng dụng của bạn gặp sự cố ngay trước mắt người dùng.
Đây chính là lúc TypeScript toả sáng! Bằng cách định kiểu (typing) cho các tham số route, chúng ta có thể thêm một lớp bảo vệ mạnh mẽ cho ứng dụng của mình.
Tại sao cần Typing Route Parameters?
Khi sử dụng các thư viện routing như React Router trong ứng dụng React SPA (Single Page Application) hoặc hệ thống routing của Next.js (cả Pages Router và App Router), các tham số route thường được cung cấp cho component của bạn dưới dạng một đối tượng. Mặc định, nếu không có TypeScript, đối tượng này có kiểu là any
hoặc một kiểu chung chung như {[key: string]: string | undefined}
.
Điều này có nghĩa là:
- Không có kiểm tra lỗi tĩnh: TypeScript sẽ không báo lỗi nếu bạn cố gắng truy cập một tham số không tồn tại (ví dụ:
params.usesrId
thay vìparams.userId
). Lỗi này chỉ xuất hiện khi chạy code. - Không có tự động hoàn thành (Autocompletion): IDE của bạn không biết các tên tham số nào có sẵn trong đối tượng
params
, làm giảm hiệu suất code. - Không rõ ràng: Đồng nghiệp (hoặc chính bạn sau này) sẽ không biết ngay lập tức component này mong đợi những tham số route nào chỉ bằng cách nhìn vào định nghĩa props hoặc cách sử dụng hook.
Bằng cách typing route parameters, chúng ta sẽ:
- Bắt lỗi sớm: TypeScript sẽ cảnh báo ngay tại thời điểm viết code (hoặc build) nếu bạn truy cập sai tên tham số hoặc sử dụng tham số sai kiểu (ví dụ: cố gắng dùng một chuỗi làm số mà chưa parse).
- Tăng năng suất: Nhận được gợi ý tự động hoàn thành chính xác từ IDE về các tham số route.
- Nâng cao tính dễ đọc và bảo trì: Code trở nên minh bạch hơn về dữ liệu đầu vào của component.
Hãy cùng xem cách thực hiện điều này với một vài ví dụ cụ thể.
Typing với React Router
Trong React Router v6 trở lên, chúng ta thường sử dụng hook useParams
để lấy các tham số route. Hook này có hỗ trợ generic type để bạn có thể truyền vào kiểu của đối tượng params mong đợi.
Bước 1: Định nghĩa kiểu cho tham số
Sử dụng interface
hoặc type
để mô tả cấu trúc của đối tượng tham số route. Các giá trị tham số từ URL luôn là chuỗi (string).
// src/types/routeParams.ts (hoặc bất kỳ file nào bạn muốn)
export interface ProductDetailParams {
productId: string; // Tên tham số phải khớp với tên trong định nghĩa route, ví dụ: /products/:productId
}
export interface UserPostParams {
userId: string; // Tên tham số khớp với :userId
postId: string; // Tên tham số khớp với :postId
}
export interface OptionalCategoryParams {
categoryId?: string; // Dấu '?' biểu thị tham số này là tùy chọn (có thể là string hoặc undefined)
}
Bước 2: Sử dụng kiểu với useParams
Import interface đã định nghĩa và truyền nó vào hook useParams
bằng cú pháp generic <T>
.
import { useParams } from 'react-router-dom';
import { ProductDetailParams, UserPostParams, OptionalCategoryParams } from './types/routeParams'; // Đường dẫn tới file types của bạn
// Ví dụ 1: Route /products/:productId
function ProductDetailPage() {
// Truyền kiểu ProductDetailParams vào useParams
const params = useParams<ProductDetailParams>();
const { productId } = params; // Hoặc destruct ngay
// ✨ Bây giờ, TypeScript biết chắc 'productId' là một 'string'.
// Nếu bạn gõ 'params.producsId', TS sẽ báo lỗi ngay!
console.log("Đang xem sản phẩm với ID:", productId);
// Note: productId là string. Nếu cần số, bạn phải parse: const idAsNumber = parseInt(productId, 10);
return (
<div>
<h1>Chi tiết sản phẩm: {productId}</h1>
{/* Logic hiển thị chi tiết sản phẩm */}
</div>
);
}
// Ví dụ 2: Route /users/:userId/posts/:postId
function UserPostDetailPage() {
const { userId, postId } = useParams<UserPostParams>();
// ✨ userId và postId đều được typed là 'string'.
console.log(`Đang xem bài viết ${postId} của người dùng ${userId}`);
return (
<div>
<h2>Bài viết {postId}</h2>
<p>của người dùng {userId}</p>
{/* Logic hiển thị bài viết */}
</div>
);
}
// Ví dụ 3: Route /products/:categoryId? (tham số tùy chọn)
function ProductListPage() {
const { categoryId } = useParams<OptionalCategoryParams>();
// ✨ categoryId được typed là 'string | undefined'.
// TS sẽ nhắc bạn kiểm tra sự tồn tại trước khi sử dụng.
console.log("categoryId:", categoryId);
return (
<div>
<h1>{categoryId ? `Sản phẩm trong danh mục ${categoryId}` : 'Tất cả sản phẩm'}</h1>
{/* Logic hiển thị danh sách sản phẩm */}
</div>
);
}
Giải thích:
- Chúng ta tạo các
interface
(ProductDetailParams
,UserPostParams
, etc.) để định nghĩa rõ ràng những tham số nào mong đợi có trên URL và kiểu dữ liệu của chúng (luôn làstring
cho các tham số route thu được từ URL). - Dấu
?
trongcategoryId?: string;
chỉ ra rằng tham sốcategoryId
là tùy chọn, tức là nó có thể có giá trị làstring
hoặcundefined
nếu không có trong URL. - Khi gọi
useParams<MyParamsInterface>()
, chúng ta báo cho TypeScript biết rằng đối tượng được trả về từ hook này sẽ tuân theo cấu trúc củaMyParamsInterface
. - Nhờ đó, khi bạn truy cập
params.productId
(hoặcproductId
sau khi destructuring), TypeScript biết chính xác kiểu của nó và có thể thực hiện kiểm tra lỗi tĩnh, cung cấp gợi ý tự động hoàn thành.
Typing với Next.js App Router
Trong Next.js App Router (từ phiên bản 13 trở lên), các tham số route từ cấu trúc thư mục sẽ được truyền xuống page hoặc layout component thông qua prop params
. Chúng ta cũng có thể định nghĩa kiểu cho prop này.
Bước 1: Định nghĩa kiểu cho tham số và props
Tương tự như React Router, định nghĩa kiểu cho các tham số. Sau đó, định nghĩa kiểu cho toàn bộ object params
và cho props của component.
// Ví dụ: Trong app/products/[productId]/page.tsx
// Định nghĩa kiểu cho các tham số route cụ thể cho page này
interface ProductPageParams {
productId: string; // Tên key phải khớp với tên thư mục động [productId]
}
// Định nghĩa kiểu cho props mà page component nhận được
interface ProductPageProps {
params: ProductPageParams;
// Nếu bạn cũng cần query params, thêm dòng này:
// searchParams: { [key: string]: string | string[] | undefined };
}
// Bước 2: Sử dụng kiểu trong định nghĩa component
// Áp dụng kiểu cho props của page component
export default function ProductPage({ params }: ProductPageProps) {
const { productId } = params; // Destructure params đã được typed
// ✨ productId đã được typed là 'string'.
console.log("Đang xem sản phẩm với ID:", productId);
return (
<div>
<h1>Chi tiết sản phẩm Next.js: {productId}</h1>
{/* Logic hiển thị chi tiết sản phẩm */}
</div>
);
}
Ví dụ với nhiều tham số:
Nếu bạn có cấu trúc app/users/[userId]/posts/[postId]/page.tsx
, các tham số sẽ là userId
và postId
.
// Ví dụ: Trong app/users/[userId]/posts/[postId]/page.tsx
interface PostPageParams {
userId: string; // Tên key khớp với [userId]
postId: string; // Tên key khớp với [postId]
}
interface PostPageProps {
params: PostPageParams;
}
export default function PostPage({ params }: PostPageProps) {
const { userId, postId } = params; // params đã được typed
// ✨ userId và postId đều được typed là 'string'.
console.log(`Đang xem bài viết ${postId} của người dùng ${userId} (Next.js)`);
return (
<div>
<h2>Bài viết {postId}</h2>
<p>của người dùng {userId}</p>
{/* Logic hiển thị bài viết */}
</div>
);
}
Giải thích:
- Trong Next.js App Router, các tham số route được tự động thu thập từ tên thư mục động (ví dụ
[productId]
) và nhóm lại thành một đối tượngparams
được truyền làm prop cho component page hoặc layout. - Chúng ta tạo
interface
(ví dụProductPageParams
,PostPageParams
) để mô tả cấu trúc mong đợi của đối tượngparams
. Các tên key trong interface phải chính xác với tên thư mục động (không có dấu ngoặc vuông[]
). - Tiếp theo, chúng ta tạo
interface
cho toàn bộ props mà component nhận được (ví dụProductPageProps
), bao gồm cảparams
và gán kiểu đã định nghĩa ở trên cho nó. - Cuối cùng, áp dụng kiểu props này vào định nghĩa function component.
- Kết quả tương tự như React Router: TypeScript biết chính xác các tham số có sẵn và kiểu của chúng, mang lại lợi ích về an toàn kiểu và tự động hoàn thành.
(Lưu ý: Next.js App Router xử lý các route "catch-all" như [[...slug]]
thành mảng (slug: string[] | undefined
), bạn cũng cần định nghĩa kiểu tương ứng cho trường hợp này.)
Comments