İçeriğe geç
react

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

Tarık Tunç
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>
      )}
    />
  );
}
İpucu: Generic componentlerde tip parametresini mümkün olduğunca daraltın. 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>
  );
}
Uyarı: 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>
  );
}
Bilgi: TypeScript'in 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.

Kaynaklar

İlgili Yazılar