Dlaczego warto migrować React z JavaScript do TypeScript?

Zanim przejdziemy do technicznych aspektów migracji, warto zastanowić się, co tak naprawdę zyskujemy, decydując się na ten krok. TypeScript to nadzbiór JavaScript, który dodaje statyczne typowanie do języka. W kontekście aplikacji React oznacza to znacznie lepsze wykrywanie błędów już na etapie pisania kodu, a nie dopiero w czasie działania aplikacji.

Główne korzyści z migracji obejmują:

  • Wykrywanie błędów w czasie kompilacji – TypeScript wyłapuje błędy typów zanim kod trafi do przeglądarki
  • Lepsza dokumentacja kodu – typy służą jako wbudowana dokumentacja, ułatwiając zrozumienie interfejsów komponentów
  • Wsparcie IDE – edytory kodu oferują znacznie lepsze autouzupełnianie i podpowiedzi
  • Bezpieczniejszy refactoring – zmiany w kodzie są mniej ryzykowne, bo TypeScript informuje o wszystkich miejscach wymagających aktualizacji
  • Lepsza skalowalność – większe zespoły i projekty znacznie łatwiej utrzymują kod z TypeScript

Przygotowanie do migracji – strategia stopniowa

Jedną z największych zalet TypeScript jest to, że możesz migrować projekt stopniowo. Nie musisz przepisywać całej aplikacji naraz – możesz zacząć od pojedynczych plików i sukcesywnie przenosić kolejne komponenty.

Istnieją dwie główne strategie migracji:

  1. Migracja całościowa (Big Bang) – przepisanie całego projektu od razu. Ryzykowna, ale pozwala na jednorazowe wdrożenie spójnych konwencji.
  2. Migracja stopniowa (Incremental) – rekomendowana dla większości projektów. Pliki JavaScript i TypeScript mogą współistnieć w tym samym projekcie.

Konfiguracja środowiska TypeScript w projekcie React

Zacznijmy od konfiguracji. Jeśli tworzysz nowy projekt, możesz użyć Create React App z szablonem TypeScript:

npx create-react-app moja-aplikacja --template typescript

Dla istniejącego projektu, zainstaluj niezbędne pakiety:

npm install --save-dev typescript @types/react @types/react-dom @types/node

Następnie utwórz plik konfiguracyjny tsconfig.json w katalogu głównym projektu:

{
  "compilerOptions": {
    "target": "es5",
    "lib": ["dom", "dom.iterable", "esnext"],
    "allowJs": true,
    "skipLibCheck": true,
    "esModuleInterop": true,
    "allowSyntheticDefaultImports": true,
    "strict": true,
    "forceConsistentCasingInFileNames": true,
    "noFallthroughCasesInSwitch": true,
    "module": "esnext",
    "moduleResolution": "node",
    "resolveJsonModule": true,
    "isolatedModules": true,
    "noEmit": true,
    "jsx": "react-jsx"
  },
  "include": ["src"]
}

Opcja allowJs: true jest kluczowa – pozwala na współistnienie plików .js i .ts w tym samym projekcie podczas migracji stopniowej.

Typowanie komponentów funkcyjnych

Gdy środowisko jest już skonfigurowane, czas na przepisywanie komponentów. Zacznij od zmiany rozszerzenia pliku z .jsx na .tsx (lub z .js na .ts dla plików bez JSX).

Przykład prostego komponentu w JavaScript:

// Button.jsx
function Button({ label, onClick, disabled }) {
  return (
    <button onClick={onClick} disabled={disabled}>
      {label}
    </button>
  );
}

Ten sam komponent w TypeScript:

// Button.tsx
interface ButtonProps {
  label: string;
  onClick: () => void;
  disabled?: boolean;
}

const Button: React.FC<ButtonProps> = ({ label, onClick, disabled = false }) => {
  return (
    <button onClick={onClick} disabled={disabled}>
      {label}
    </button>
  );
};

Warto zaznaczyć, że współcześnie preferuje się bezpośrednie typowanie props zamiast używania React.FC:

// Nowoczesne podejście
function Button({ label, onClick, disabled = false }: ButtonProps) {
  return (
    <button onClick={onClick} disabled={disabled}>
      {label}
    </button>
  );
}

Typowanie hooków React

Kolejnym ważnym aspektem jest poprawne typowanie wbudowanych hooków React. Przyjrzyjmy się najczęściej używanym.

useState

TypeScript często potrafi wywnioskować typ na podstawie wartości początkowej, jednak warto być eksplicytnym, szczególnie gdy stan może być null lub undefined:

// TypeScript sam wywnioskuje typ 'number'
const [count, setCount] = useState(0);

// Eksplicytne typowanie dla bardziej złożonych przypadków
const [user, setUser] = useState<User | null>(null);

// Typowanie tablicy
const [items, setItems] = useState<string[]>([]);

useRef

Hook useRef wymaga szczególnej uwagi przy typowaniu:

// Ref do elementu DOM
const inputRef = useRef<HTMLInputElement>(null);

// Ref do przechowywania wartości mutowalnej
const timerRef = useRef<number | null>(null);

useReducer

Dla bardziej złożonego zarządzania stanem z useReducer:

type State = {
  count: number;
  loading: boolean;
};

type Action =
  | { type: 'INCREMENT' }
  | { type: 'DECREMENT' }
  | { type: 'SET_LOADING'; payload: boolean };

function reducer(state: State, action: Action): State {
  switch (action.type) {
    case 'INCREMENT':
      return { ...state, count: state.count + 1 };
    case 'DECREMENT':
      return { ...state, count: state.count - 1 };
    case 'SET_LOADING':
      return { ...state, loading: action.payload };
    default:
      return state;
  }
}

const [state, dispatch] = useReducer(reducer, { count: 0, loading: false });

Typowanie zdarzeń i obsługi formularzy

Obsługa zdarzeń w TypeScript wymaga znajomości odpowiednich typów. Oto najczęściej używane:

// Zdarzenie kliknięcia
const handleClick = (event: React.MouseEvent<HTMLButtonElement>) => {
  event.preventDefault();
  console.log('Kliknięto!');
};

// Zdarzenie zmiany inputa
const handleChange = (event: React.ChangeEvent<HTMLInputElement>) => {
  setValue(event.target.value);
};

// Zdarzenie submit formularza
const handleSubmit = (event: React.FormEvent<HTMLFormElement>) => {
  event.preventDefault();
  // obsługa formularza
};

Typowanie Context API

Context API jest niezwykle popularnym rozwiązaniem w React. Oto jak poprawnie je otypować:

interface ThemeContextType {
  theme: 'light' | 'dark';
  toggleTheme: () => void;
}

const ThemeContext = React.createContext<ThemeContextType | undefined>(undefined);

export function useTheme(): ThemeContextType {
  const context = useContext(ThemeContext);
  if (context === undefined) {
    throw new Error('useTheme must be used within a ThemeProvider');
  }
  return context;
}

export function ThemeProvider({ children }: { children: React.ReactNode }) {
  const [theme, setTheme] = useState<'light' | 'dark'>('light');

  const toggleTheme = () => {
    setTheme(prev => prev === 'light' ? 'dark' : 'light');
  };

  return (
    <ThemeContext.Provider value={{ theme, toggleTheme }}>
      {children}
    </ThemeContext.Provider>
  );
}

Typowanie niestandardowych hooków

Tworzenie własnych hooków z TypeScript to doskonały sposób na enkapsulację logiki z pełnym bezpieczeństwem typów:

interface UseFetchResult<T> {
  data: T | null;
  loading: boolean;
  error: string | null;
}

function useFetch<T>(url: string): UseFetchResult<T> {
  const [data, setData] = useState<T | null>(null);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState<string | null>(null);

  useEffect(() => {
    fetch(url)
      .then(res => res.json())
      .then((data: T) => {
        setData(data);
        setLoading(false);
      })
      .catch((err: Error) => {
        setError(err.message);
        setLoading(false);
      });
  }, [url]);

  return { data, loading, error };
}

Częste problemy podczas migracji i jak je rozwiązać

Podczas migracji możesz napotkać kilka typowych problemów:

Problem z bibliotekami bez typów

Jeśli używasz biblioteki, która nie ma definicji typów, możesz tymczasowo zadeklarować ją jako moduł:

// declarations.d.ts
declare module 'nazwa-biblioteki';

Lub zainstalować definicje typów z @types/:

npm install --save-dev @types/lodash

Stopniowe wyłączanie trybu strict

Jeśli tryb strict generuje zbyt wiele błędów na początku, możesz go stopniowo włączać poprzez selektywne opcje:

{
  "compilerOptions": {
    "strictNullChecks": true,
    "strictFunctionTypes": true
    // pozostałe opcje strict dodawaj stopniowo
  }
}

Narzędzia wspierające migrację

Warto skorzystać z narzędzi, które automatyzują część pracy podczas migracji:

  • ts-migrate – narzędzie od Airbnb, które automatycznie konwertuje pliki JavaScript do TypeScript
  • TypeStat – automatycznie dodaje typy do istniejącego kodu
  • ESLint z @typescript-eslint – pomaga zachować spójność kodu TypeScript

Podsumowanie i najlepsze praktyki

Migracja z JavaScript do TypeScript w projekcie React to inwestycja, która zwraca się wielokrotnie w postaci mniejszej liczby błędów, lepszej utrzymywalności i przyjemniejszej pracy z kodem. Pamiętaj o kilku kluczowych zasadach:

  • Migruj stopniowo – nie musisz przepisywać wszystkiego naraz
  • Zacznij od strict: true w nowych projektach, w starszych włączaj opcje stopniowo
  • Unikaj używania any – to zaprzeczenie idei TypeScript
  • Preferuj interface dla obiektów i type dla unii i bardziej złożonych typów
  • Dokumentuj skomplikowane typy komentarzami JSDoc
  • Korzystaj z generycznych typów (<T>) dla wielokrotnego użytku komponentów i hooków

TypeScript w React to dziś standard w profesjonalnym tworzeniu aplikacji webowych. Niezależnie od rozmiaru projektu, korzyści przeważają nad kosztami migracji. Zacznij od małych kroków i ciesz się bezpieczniejszym, bardziej przewidywalnym kodem.