Typing Props đúng cách
#interface hay type?
Câu hỏi này xuất hiện ở mọi team. Câu trả lời thực tế: dùng type cho props.
// ✅ Preferred
type ButtonProps = {
label: string;
onClick: () => void;
variant?: 'primary' | 'secondary';
disabled?: boolean;
};
// ❌ interface cũng work nhưng không cần thiết
interface ButtonProps {
label: string;
// ...
}type linh hoạt hơn: có thể dùng union, intersection, mapped types. interface chỉ tốt hơn khi cần extends hoặc declaration merging — rất hiếm với props.
#Đừng dùng React.FC
// ❌ Cũ và có vấn đề
const Button: React.FC<ButtonProps> = ({ label }) => {
return <button>{label}</button>;
};
// ✅ Đơn giản và rõ ràng hơn
function Button({ label, onClick, variant = 'primary', disabled }: ButtonProps) {
return (
<button
onClick={onClick}
disabled={disabled}
data-variant={variant}
>
{label}
</button>
);
}React.FC implicitly kèm children (đã bị bỏ từ React 18), và return type JSX.Element quá chặt — không cho phép return null.
#Typing children
Khi component nhận children, khai báo tường minh:
type CardProps = {
title: string;
children: React.ReactNode;
};
function Card({ title, children }: CardProps) {
return (
<div className="card">
<h2>{title}</h2>
<div>{children}</div>
</div>
);
}React.ReactNode chấp nhận mọi thứ: string, number, JSX, null, array, fragment — đây là type đúng cho children trong hầu hết trường hợp.
#Extending HTML element props
Pattern cực kỳ hữu ích khi build component library:
type InputProps = React.InputHTMLAttributes<HTMLInputElement> & {
label: string;
error?: string;
};
function Input({ label, error, ...rest }: InputProps) {
return (
<div>
<label>{label}</label>
<input {...rest} aria-invalid={!!error} />
{error && <p role="alert">{error}</p>}
</div>
);
}Giờ Input nhận tất cả props của <input> native (placeholder, value, onChange, disabled, ...) mà không cần khai báo lại từng cái.
#Discriminated union cho variant components
type AlertProps =
| { type: 'info'; message: string }
| { type: 'error'; message: string; onRetry: () => void }
| { type: 'success'; message: string; onDismiss?: () => void };
function Alert(props: AlertProps) {
if (props.type === 'error') {
// TypeScript biết onRetry tồn tại ở đây
return (
<div role="alert">
{props.message}
<button onClick={props.onRetry}>Retry</button>
</div>
);
}
return <div role="status">{props.message}</div>;
}Discriminated union giúp TypeScript narrow type tự động — không cần optional chaining hay type assertions.