TypeScript Best Practices cho React Developers

6 phút đọcCong Dinh
TypeScript Best Practices cho React Developers

TypeScript Best Practices cho React Developers

TypeScript đã trở thành standard trong React development. Trong bài viết này, chúng ta sẽ khám phá các best practices giúp bạn viết code TypeScript tốt hơn, maintainable hơn và ít bugs hơn.

Tại sao TypeScript quan trọng?

TypeScript mang lại nhiều lợi ích:

  • Type safety: Catch errors lúc compile time thay vì runtime
  • Better IDE support: Autocomplete và IntelliSense tuyệt vời
  • Refactoring confidence: Dễ dàng refactor code mà không lo break
  • Self-documenting code: Types serve như documentation

Best Practice 1: Sử dụng Interface cho Props

❌ Không nên

type ButtonProps = {
  text: string;
  onClick: any; // Tránh dùng any!
};

✅ Nên làm

interface ButtonProps {
  text: string;
  onClick: (event: React.MouseEvent<HTMLButtonElement>) => void;
  variant?: "primary" | "secondary" | "outline";
  disabled?: boolean;
  children?: React.ReactNode;
}
 
export function Button({ text, onClick, variant = "primary", disabled = false }: ButtonProps) {
  return (
    <button 
      className={`btn btn-${variant}`}
      onClick={onClick}
      disabled={disabled}
    >
      {text}
    </button>
  );
}

Tại sao?

  • Interface có thể extend và merge dễ dàng
  • Naming convention rõ ràng: ComponentNameProps
  • Optional properties với ?
  • Default values trong destructuring

Best Practice 2: Generic Components

Generic components giúp tạo reusable components với type safety.

Example: Generic List Component

interface ListProps<T> {
  items: T[];
  renderItem: (item: T, index: number) => React.ReactNode;
  keyExtractor: (item: T) => string | number;
  emptyMessage?: string;
}
 
function List<T>({ items, renderItem, keyExtractor, emptyMessage = "No items" }: ListProps<T>) {
  if (items.length === 0) {
    return <p>{emptyMessage}</p>;
  }
 
  return (
    <ul>
      {items.map((item, index) => (
        <li key={keyExtractor(item)}>
          {renderItem(item, index)}
        </li>
      ))}
    </ul>
  );
}
 
// Usage
interface User {
  id: number;
  name: string;
  email: string;
}
 
function UserList({ users }: { users: User[] }) {
  return (
    <List<User>
      items={users}
      renderItem={(user) => (
        <div>
          <h3>{user.name}</h3>
          <p>{user.email}</p>
        </div>
      )}
      keyExtractor={(user) => user.id}
      emptyMessage="Chưa có users nào"
    />
  );
}

Best Practice 3: Union Types cho Props Variants

Sử dụng union types để enforce correct prop combinations.

Example: Button với Multiple Variants

type BaseButtonProps = {
  children: React.ReactNode;
  disabled?: boolean;
  className?: string;
};
 
type PrimaryButtonProps = BaseButtonProps & {
  variant: "primary";
  onClick: () => void;
};
 
type LinkButtonProps = BaseButtonProps & {
  variant: "link";
  href: string;
  target?: "_blank" | "_self";
};
 
type SubmitButtonProps = BaseButtonProps & {
  variant: "submit";
  form: string;
};
 
type ButtonProps = PrimaryButtonProps | LinkButtonProps | SubmitButtonProps;
 
function Button(props: ButtonProps) {
  const { variant, children, disabled, className } = props;
 
  if (variant === "link") {
    return (
      <a 
        href={props.href} 
        target={props.target}
        className={className}
      >
        {children}
      </a>
    );
  }
 
  if (variant === "submit") {
    return (
      <button 
        type="submit"
        form={props.form}
        disabled={disabled}
        className={className}
      >
        {children}
      </button>
    );
  }
 
  return (
    <button 
      onClick={props.onClick}
      disabled={disabled}
      className={className}
    >
      {children}
    </button>
  );
}

TypeScript sẽ enforce đúng props cho từng variant!

Best Practice 4: Utility Types

Tận dụng built-in utility types của TypeScript.

Partial<T>

interface User {
  id: number;
  name: string;
  email: string;
  bio: string;
}
 
// Update user - không cần tất cả fields
function updateUser(id: number, updates: Partial<User>) {
  // API call to update user
}
 
updateUser(1, { name: "New Name" }); // ✅ OK
updateUser(1, { email: "new@email.com", bio: "New bio" }); // ✅ OK

Pick<T, K>Omit<T, K>

// Chỉ lấy một số fields
type UserPreview = Pick<User, "id" | "name">;
 
// Loại bỏ một số fields
type UserWithoutId = Omit<User, "id">;
 
// Usage in components
interface UserCardProps {
  user: UserPreview; // Chỉ cần id và name
}

Record<K, T>

// Map từ string key đến value type
type UserRole = "admin" | "editor" | "viewer";
 
const permissions: Record<UserRole, string[]> = {
  admin: ["read", "write", "delete"],
  editor: ["read", "write"],
  viewer: ["read"],
};

Best Practice 5: Custom Hooks với TypeScript

Type your custom hooks properly.

import { useState, useEffect } from 'react';
 
interface UseFetchResult<T> {
  data: T | null;
  loading: boolean;
  error: Error | null;
  refetch: () => void;
}
 
function useFetch<T>(url: string): UseFetchResult<T> {
  const [data, setData] = useState<T | null>(null);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState<Error | null>(null);
 
  const fetchData = async () => {
    try {
      setLoading(true);
      const response = await fetch(url);
      if (!response.ok) {
        throw new Error(`HTTP error! status: ${response.status}`);
      }
      const result = await response.json();
      setData(result);
      setError(null);
    } catch (e) {
      setError(e instanceof Error ? e : new Error('Unknown error'));
      setData(null);
    } finally {
      setLoading(false);
    }
  };
 
  useEffect(() => {
    fetchData();
  }, [url]);
 
  return { data, loading, error, refetch: fetchData };
}
 
// Usage
interface User {
  id: number;
  name: string;
}
 
function UserProfile({ userId }: { userId: number }) {
  const { data: user, loading, error } = useFetch<User>(`/api/users/${userId}`);
 
  if (loading) return <div>Loading...</div>;
  if (error) return <div>Error: {error.message}</div>;
  if (!user) return null;
 
  return <div>{user.name}</div>;
}

Best Practice 6: Event Handlers

Type event handlers correctly cho better type safety.

interface FormProps {
  onSubmit: (data: FormData) => void;
}
 
function MyForm({ onSubmit }: FormProps) {
  // Form submit handler
  const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
    e.preventDefault();
    const formData = new FormData(e.currentTarget);
    onSubmit(formData);
  };
 
  // Input change handler
  const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
    console.log(e.target.value);
  };
 
  // Button click handler
  const handleClick = (e: React.MouseEvent<HTMLButtonElement>) => {
    console.log('Button clicked', e.currentTarget);
  };
 
  return (
    <form onSubmit={handleSubmit}>
      <input type="text" onChange={handleChange} />
      <button onClick={handleClick}>Submit</button>
    </form>
  );
}

Best Practice 7: Avoid Type Assertions

❌ Không nên

const value = JSON.parse(jsonString) as User; // Unsafe!

✅ Nên làm

import { z } from 'zod';
 
// Define schema với Zod
const UserSchema = z.object({
  id: z.number(),
  name: z.string(),
  email: z.string().email(),
});
 
type User = z.infer<typeof UserSchema>;
 
function parseUser(jsonString: string): User {
  const parsed = JSON.parse(jsonString);
  return UserSchema.parse(parsed); // Runtime validation!
}

Best Practice 8: Discriminated Unions

Sử dụng discriminated unions cho complex state management.

type RequestState<T> =
  | { status: 'idle' }
  | { status: 'loading' }
  | { status: 'success'; data: T }
  | { status: 'error'; error: string };
 
function DataComponent() {
  const [state, setState] = useState<RequestState<User>>({ status: 'idle' });
 
  // TypeScript biết chính xác properties available cho mỗi status
  if (state.status === 'loading') {
    return <div>Loading...</div>;
  }
 
  if (state.status === 'error') {
    return <div>Error: {state.error}</div>; // TypeScript knows 'error' exists
  }
 
  if (state.status === 'success') {
    return <div>{state.data.name}</div>; // TypeScript knows 'data' exists
  }
 
  return <button onClick={() => setState({ status: 'loading' })}>Load Data</button>;
}

Kết luận

TypeScript best practices giúp bạn:

Tránh bugs: Type safety catch errors sớm
Code maintainable hơn: Self-documenting code
Developer experience tốt hơn: Autocomplete và IntelliSense
Refactoring dễ dàng: Confidence khi thay đổi code

Key Takeaways

  1. Sử dụng Interface cho component props
  2. Leverage generic types cho reusable components
  3. Sử dụng union types cho variant props
  4. Tận dụng utility types (Partial, Pick, Omit, Record)
  5. Type custom hooks properly
  6. Type event handlers correctly
  7. Avoid type assertions, use validation instead
  8. Sử dụng discriminated unions cho complex state

Next Steps

  • Practice các patterns này trong projects của bạn
  • Explore thêm advanced TypeScript features
  • Check out TypeScript Handbook

Happy coding! 🚀

Cong Dinh

Cong Dinh

Technology Consultant | Trainer | Solution Architect

Với hơn 10 năm kinh nghiệm trong phát triển web và cloud architecture, tôi giúp doanh nghiệp xây dựng giải pháp công nghệ hiện đại và bền vững. Chuyên môn: Next.js, TypeScript, AWS, và Solution Architecture.

Bài viết liên quan