TypeScript không biết nút màu đỏ có khiến người dùng bối rối hay không. Nó chỉ giúp mình chặn những lỗi như truyền sai biến thể props, bỏ quên một nhánh hoặc gõ sai tên cấu hình. Ba pattern dưới đây là những thứ mình muốn nhớ khi viết component, không phải một bộ sưu tập type gymnastics.
Các snippet được chạy qua tsc với TypeScript 5.9 và type của React 19. Mỗi ví dụ cố tình nhỏ để nhìn thấy quan hệ kiểu, không giả làm một ứng dụng hoàn chỉnh.
Props có nhánh loại trừ nhau
Nhiều component có biến thể. Biến thể chính có thêm một prop, biến thể còn lại thì không. Nếu mọi prop đều optional, người gọi có thể truyền tổ hợp vô nghĩa và TypeScript vẫn im.
Union phân biệt gắn prop phụ vào đúng nhánh:
import type { ReactNode } from "react";
type ButtonProps = {
children: ReactNode;
onClick?: () => void;
} & ({ variant: "primary"; shortcut?: string } | { variant: "quiet" });
export function Button(props: ButtonProps) {
if (props.variant === "primary") {
return (
<button type="button" onClick={props.onClick}>
{props.children}
{props.shortcut ? <kbd>{props.shortcut}</kbd> : null}
</button>
);
}
return (
<button type="button" onClick={props.onClick}>
{props.children}
</button>
);
}Với variant: "primary", shortcut tồn tại. Với variant: "quiet", gõ props.shortcut là lỗi. Điều kiện props.variant === "primary" thu hẹp kiểu, nên nhánh còn lại không cần ép as.
Pattern này hợp với trạng thái hiển thị hơn là với style. Đừng tạo union chỉ để chọn màu. Hãy dùng nó khi các tổ hợp props loại trừ nhau.
Generic khi danh sách không biết trước kiểu phần tử
Một list tái sử dụng không nên nhận any[]. Generic giữ kiểu phần tử từ dữ liệu đi vào hàm lấy key và hàm render:
type ListProps<T> = {
items: readonly T[];
getKey: (item: T) => string;
renderItem: (item: T) => string;
};
export function TextList<T>({ items, getKey, renderItem }: ListProps<T>) {
return (
<ul>
{items.map((item) => (
<li key={getKey(item)}>{renderItem(item)}</li>
))}
</ul>
);
}Người gọi truyền items là mảng user thì getKey nhận user. Không cần khai báo lại kiểu ở hai chỗ. Generic trở nên có hại khi chỉ có một kiểu sử dụng: lúc đó một component cụ thể dễ đọc hơn một hàm bốn tham số kiểu.
renderItem ở ví dụ trả về string để ví dụ khỏi phải giải thích children. Khi cần JSX, kiểu trả về là ReactNode, và component nên để người gọi truyền children hoặc một render prop có chủ đích. Đừng generic hóa mọi component “cho sau này”.
satisfies giữ literal, vẫn kiểm tra hình dạng
as Route ép kiểu và có thể nuốt lỗi. satisfies kiểm tra giá trị khớp Route nhưng vẫn giữ literal, nên path còn là "/blog/" chứ không bị nới thành string.
type Route = {
path: `/${string}`;
label: string;
};
export const routes = [
{ path: "/blog/", label: "Blog" },
{ path: "/projects/", label: "Dự án" },
] as const satisfies readonly Route[];
export const blogPath = routes[0].path;Dùng satisfies cho bảng route, bảng theme, hoặc map phím. Đừng dùng nó để bịa một object cho khớp kiểu khi dữ liệu thật chưa có. Nếu giá trị thiếu field, hãy sửa dữ liệu, đừng ép.
unknown thay cho as khi dữ liệu đến từ ngoài
JSON, localStorage và response.json() không có kiểu thật cho đến khi bạn kiểm tra. as User chỉ bảo compiler im. Một type guard đọc field và trả về value is User thì cả compiler lẫn runtime cùng một điều kiện.
Hãy viết guard cho đúng các field bạn sẽ dùng, không cho cả một schema tưởng tượng. Nếu chỉ hiển thị name, hãy kiểm tra name là string. Phần còn lại của payload không cần được tin.
any tắt kiểm tra trên cả biểu thức phía sau. unknown bắt bạn thu hẹp trước khi đọc field. Với dữ liệu ngoài biên, unknown là mặc định đúng.
Những chỗ không đáng thêm kiểu
Không cần generic cho một nút chỉ nhận children và onClick. ComponentProps<"button"> đã có sẵn kiểu sự kiện nếu bạn muốn chuyển tiếp props của phần tử HTML.
Không cần mô hình hóa lại toàn bộ state của một màn hình trong một type 30 field nếu state đó chỉ sống trong một component. Để type gần chỗ dùng. Khi state được chia sẻ, xem bài quản lý state để chọn Context, Zustand hoặc Jotai trước, rồi mới viết type cho store.
Không dùng as để qua một lỗi bạn chưa hiểu. Lỗi đó thường là union chưa đủ hẹp, hoặc dữ liệu có thể vắng. Ép kiểu dời lỗi sang lúc chạy, đúng chỗ người dùng nhìn thấy.
React 19 cho phép khai báo ref như một prop thường trên function component. forwardRef vẫn chạy, nhưng đừng thêm nó chỉ vì một snippet cũ. Nếu component không cần ref, đừng khai báo ref.
Cách áp dụng vừa đủ
- Bật
strictvà sửa lỗi hiện có trước khi thêm abstraction. - Dùng union phân biệt cho biến thể loại trừ nhau.
- Dùng generic khi có từ hai kiểu phần tử thật, không phải một kiểu giả định.
- Dùng
satisfiescho cấu hình tĩnh. - Dùng type guard ở biên JSON và storage.
TypeScript không thay thiết kế component. Nó làm thiết kế đó khó bị gọi sai. Nếu một type dài hơn phần render, thường là props đang ôm quá nhiều việc. Tách component trước khi viết thêm generic.
Bài React và Next.js nói về chỗ đặt component đó trong một website. Kiểu của props không đổi giữa SPA và App Router; chỉ chỉ thị client và server là đổi.