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.