İçeriğe geç
Teknik YazıTypeScript

TypeScript ile Daha İyi React Bileşenleri

React bileşenlerini sadece tip güvenli hale getirmek değil, daha anlaşılır ve daha derli toplu yazmak için işime en çok yarayan TypeScript alışkanlıkları.

20 Şubat 20248 dk okuma

TypeScript’in React tarafındaki asıl güzelliği sadece hata yakalaması değil. Bileşenin ne istediğini açık açık göstermesi. İyi yazılmış tipler, bileşenin yanına konmuş kısa bir açıklama gibi çalışıyor. Özellikle birkaç ay sonra koda geri döndüğünde bunun rahatlığını net hissediyorsun.

Props Tanımlarken interface mi, type mı?

Bu konuda tek doğru yok. Ama bileşen propslarında çoğu zaman `interface` bana daha okunur geliyor. Özellikle ekip içinde çalışırken hata mesajlarının daha düzgün görünmesi ve genişletmenin kolay olması günlük kullanımda fark yaratıyor.

tsx
interface ButtonProps {
  label: string
  variant?: 'primary' | 'outline' | 'ghost'
  size?: 'sm' | 'md' | 'lg'
  disabled?: boolean
  onClick?: () => void
}

export function Button({ label, variant = 'primary', size = 'md', ...rest }: ButtonProps) {
  return <button className={cn(base, variants[variant], sizes[size])} {...rest}>{label}</button>
}

children Alanını Bilinçli Yazmak

Birçok projede `PropsWithChildren` otomatik alışkanlık olmuş durumda. Ama children alanını açıkça yazmak, bileşenin gerçekten ne kadar esnek olması gerektiğini düşünmeye zorluyor. Bu da bileşen arayüzünü daha temiz kurmana yardım ediyor.

tsx
import { ReactNode } from 'react'

interface CardProps {
  title: string
  children: ReactNode
  footer?: ReactNode
  className?: string
}

interface LabelProps {
  children: string
}

Generic Bileşenler

Aynı yapıyı farklı veri tipleriyle tekrar tekrar kullanacaksan generics ciddi rahatlık sağlıyor. Liste, tablo ya da seçim bileşeni gibi tekrar eden yerlerde hem kod tekrarını azaltıyor hem de tip güvenliğini kaybetmiyorsun.

tsx
interface ListProps<T> {
  items: T[]
  renderItem: (item: T, index: number) => ReactNode
  keyExtractor: (item: T) => string
  emptyText?: string
}

export function List<T>({ items, renderItem, keyExtractor, emptyText = 'Sonuç yok' }: ListProps<T>) {
  if (items.length === 0) return <p>{emptyText}</p>
  return (
    <ul>
      {items.map((item, i) => (
        <li key={keyExtractor(item)}>{renderItem(item, i)}</li>
      ))}
    </ul>
  )
}

İpucu

`as const` küçük bir ayrıntı gibi görünür ama özellikle varyant ve boyut gibi sınırlı seçeneklerde işi çok toparlar.

Custom Hook Tipleri

tsx
function useLocalStorage<T>(key: string, initial: T) {
  const [value, setValue] = useState<T>(() => {
    if (typeof window === 'undefined') return initial
    try {
      const item = localStorage.getItem(key)
      return item ? (JSON.parse(item) as T) : initial
    } catch {
      return initial
    }
  })

  const set = (v: T | ((prev: T) => T)) => {
    setValue(v)
    localStorage.setItem(key, JSON.stringify(typeof v === 'function' ? (v as (p: T) => T)(value) : v))
  }

  return [value, set] as const
}

Hook döndürürken tuple yapısını korumak küçük bir ayrıntı gibi görünür ama kullanım tarafını doğrudan etkiler. Yanlış çıkarım olduğunda, hook’u kullanan yerde gereksiz kontrol yazmak zorunda kalıyorsun.

Koşullu Props İçin Discriminated Union

Bazı bileşenler tek bir modda yaşamıyor. Bilgi, onay ve hata gibi farklı halleri oluyor. Böyle durumlarda discriminated union yaklaşımı çok temiz çalışıyor. Yanlış prop kombinasyonları daha kodu yazarken önüne düşüyor.

tsx
type AlertProps =
  | { variant: 'info';    message: string }
  | { variant: 'confirm'; message: string; onConfirm: () => void; onCancel: () => void }
  | { variant: 'error';   message: string; error: Error }