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.
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.
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.
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
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.
type AlertProps =
| { variant: 'info'; message: string }
| { variant: 'confirm'; message: string; onConfirm: () => void; onCancel: () => void }
| { variant: 'error'; message: string; error: Error }