React + TypeScript: Tip Güvenli Component Geliştirme

React + TypeScript: Tip Güvenli Component Geliştirme
TypeScript, React projelerine tip güvenliği ekleyerek hataları daha kod yazım aşamasında yakalamanızı sağlar. Otomatik tamamlama, refactoring kolaylığı ve daha iyi dokümantasyon gibi avantajlarıyla modern React geliştirmede neredeyse standart haline gelmiştir. Bu rehberde, React componentlerini TypeScript ile tip güvenli şekilde geliştirmenin en iyi pratiklerini adım adım ele alacağız.
Component Props Tipleme
TypeScript ile React componentlerinin props'larını tanımlamak, component API'nizin dokümantasyonu niteliğindedir. Interface veya type alias kullanarak props'ları tanımlayabilirsiniz.
import { ReactNode, ComponentPropsWithoutRef, forwardRef } from 'react';
// Temel props tipleme
interface ButtonProps {
variant: 'primary' | 'secondary' | 'danger';
size?: 'sm' | 'md' | 'lg';
isLoading?: boolean;
leftIcon?: ReactNode;
children: ReactNode;
onClick?: () => void;
}
function Button({ variant, size = 'md', isLoading = false, leftIcon, children, onClick }: ButtonProps) {
const sizeClasses = {
sm: 'px-3 py-1 text-sm',
md: 'px-4 py-2 text-base',
lg: 'px-6 py-3 text-lg',
};
const variantClasses = {
primary: 'bg-blue-600 text-white hover:bg-blue-700',
secondary: 'bg-gray-200 text-gray-800 hover:bg-gray-300',
danger: 'bg-red-600 text-white hover:bg-red-700',
};
return (
<button
onClick={onClick}
disabled={isLoading}
className={`rounded font-medium ${sizeClasses[size]} ${variantClasses[variant]} ${isLoading ? 'opacity-50 cursor-not-allowed' : ''}`}
>
{leftIcon && <span className="mr-2">{leftIcon}</span>}
{isLoading ? 'Yükleniyor...' : children}
</button>
);
}
// HTML attribute'larını da destekleyen polymorphic button
interface AdvancedButtonProps extends ComponentPropsWithoutRef<'button'> {
variant: 'primary' | 'secondary' | 'danger';
isLoading?: boolean;
}
const AdvancedButton = forwardRef<HTMLButtonElement, AdvancedButtonProps>(
({ variant, isLoading, children, className, ...rest }, ref) => {
return (
<button ref={ref} className={`btn btn-${variant} ${className ?? ''}`} disabled={isLoading} {...rest}>
{isLoading ? 'Yükleniyor...' : children}
</button>
);
}
);
AdvancedButton.displayName = 'AdvancedButton';
Generic Components
Generic tipler, farklı veri tipleriyle çalışabilen esnek componentler yazmanızı sağlar. Özellikle liste, tablo ve select gibi componentlerde çok kullanışlıdır.
import { useState, ReactNode } from 'react';
// Generic Select component
interface SelectOption<T> {
value: T;
label: string;
}
interface SelectProps<T> {
options: SelectOption<T>[];
value: T | null;
onChange: (value: T) => void;
placeholder?: string;
renderOption?: (option: SelectOption<T>) => ReactNode;
}
function Select<T extends string | number>({
options,
value,
onChange,
placeholder = 'Seçiniz...',
renderOption,
}: SelectProps<T>) {
return (
<select
value={value ?? ''}
onChange={(e) => {
const selected = options.find((opt) => String(opt.value) === e.target.value);
if (selected) onChange(selected.value);
}}
className="border rounded px-3 py-2 w-full"
>
<option value="" disabled>{placeholder}</option>
{options.map((option) => (
<option key={String(option.value)} value={String(option.value)}>
{renderOption ? renderOption(option) : option.label}
</option>
))}
</select>
);
}
// Generic List component
interface ListProps<T> {
items: T[];
renderItem: (item: T, index: number) => ReactNode;
keyExtractor: (item: T) => string;
emptyMessage?: string;
}
function List<T>({ items, renderItem, keyExtractor, emptyMessage = 'Veri bulunamadı' }: ListProps<T>) {
if (items.length === 0) {
return <p className="text-gray-500 text-center py-8">{emptyMessage}</p>;
}
return (
<ul className="divide-y">
{items.map((item, index) => (
<li key={keyExtractor(item)}>{renderItem(item, index)}</li>
))}
</ul>
);
}
// Kullanım
interface User {
id: string;
name: string;
email: string;
}
function UserList() {
const [users] = useState<User[]>([
{ id: '1', name: 'Tarık', email: 'tarik@example.com' },
{ id: '2', name: 'Ahmet', email: 'ahmet@example.com' },
]);
return (
<List<User>
items={users}
keyExtractor={(user) => user.id}
renderItem={(user) => (
<div className="py-3">
<h3 className="font-semibold">{user.name}</h3>
<p className="text-gray-500">{user.email}</p>
</div>
)}
/>
);
}
T extends string | number gibi kısıtlamalar, component'in yanlış kullanımını derleme zamanında engeller.
Custom Hook Tipleme
import { useState, useEffect, useCallback } from 'react';
// Generic fetch hook
interface UseFetchResult<T> {
data: T | null;
isLoading: boolean;
error: string | null;
refetch: () => void;
}
function useFetch<T>(url: string, options?: RequestInit): UseFetchResult<T> {
const [data, setData] = useState<T | null>(null);
const [isLoading, setIsLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const fetchData = useCallback(async () => {
setIsLoading(true);
setError(null);
try {
const response = await fetch(url, options);
if (!response.ok) {
throw new Error(`HTTP Error: ${response.status}`);
}
const result: T = await response.json();
setData(result);
} catch (err) {
setError(err instanceof Error ? err.message : 'Bilinmeyen hata');
} finally {
setIsLoading(false);
}
}, [url]);
useEffect(() => {
fetchData();
}, [fetchData]);
return { data, isLoading, error, refetch: fetchData };
}
// Discriminated Union ile durum yönetimi
type AsyncState<T> =
| { status: 'idle' }
| { status: 'loading' }
| { status: 'success'; data: T }
| { status: 'error'; error: string };
function useAsync<T>() {
const [state, setState] = useState<AsyncState<T>>({ status: 'idle' });
const execute = useCallback(async (promise: Promise<T>) => {
setState({ status: 'loading' });
try {
const data = await promise;
setState({ status: 'success', data });
} catch (err) {
setState({ status: 'error', error: err instanceof Error ? err.message : 'Hata' });
}
}, []);
return { state, execute };
}
// Kullanım
function ProductPage() {
const { data: products, isLoading, error } = useFetch<Product[]>('/api/products');
if (isLoading) return <div>Yükleniyor...</div>;
if (error) return <div>Hata: {error}</div>;
return (
<div>
{products?.map((p) => <div key={p.id}>{p.name}</div>)}
</div>
);
}
React.FC (FunctionComponent) tipini kullanmaktan kaçının. Bu tip implicit children prop'u ve defaultProps ile sorunlara yol açabilir. Bunun yerine props'ları doğrudan fonksiyon parametresinde tanımlayın.
Event Handler Tipleme
import { ChangeEvent, FormEvent, MouseEvent, KeyboardEvent } from 'react';
function FormExample() {
const handleInputChange = (e: ChangeEvent<HTMLInputElement>) => {
console.log(e.target.value);
};
const handleSelectChange = (e: ChangeEvent<HTMLSelectElement>) => {
console.log(e.target.value);
};
const handleSubmit = (e: FormEvent<HTMLFormElement>) => {
e.preventDefault();
const formData = new FormData(e.currentTarget);
console.log(Object.fromEntries(formData));
};
const handleButtonClick = (e: MouseEvent<HTMLButtonElement>) => {
console.log('Button clicked at:', e.clientX, e.clientY);
};
const handleKeyDown = (e: KeyboardEvent<HTMLInputElement>) => {
if (e.key === 'Enter') {
console.log('Enter pressed');
}
};
return (
<form onSubmit={handleSubmit}>
<input onChange={handleInputChange} onKeyDown={handleKeyDown} />
<select onChange={handleSelectChange}>
<option value="a">A</option>
<option value="b">B</option>
</select>
<button type="submit" onClick={handleButtonClick}>Gönder</button>
</form>
);
}
satisfies operatörü (4.9+), type assertion yerine kullanılabilir ve tip güvenliğini korurken narrowing'i de mümkün kılar.
Sonuç
React ve TypeScript birlikte kullanıldığında, daha güvenilir, daha okunabilir ve daha bakımı kolay uygulamalar oluşturabilirsiniz. Props tipleme ile component API'nizi dokümante edin, generic tipler ile esnek componentler yazın, discriminated union'lar ile state durumlarını güvenli yönetin. TypeScript'in sağladığı otomatik tamamlama ve hata tespiti, geliştirme hızınızı önemli ölçüde artıracaktır.