本文 AI 產出,尚未審核

React + TypeScript(實務應用)

FC / Props / State 型別


簡介

在 React 專案中加入 TypeScript,最直接的收益就是編譯期就能捕捉錯誤,提升程式碼可讀性與維護性。
然而,若只是把 .js 檔改成 .tsx,卻沒有正確為 Function Component (FC)propsstate 加上型別,仍然會遇到許多執行時的問題。
本篇文章聚焦在「FC / Props / State」三大核心,從型別宣告的基本語法到實務中的常見陷阱與最佳實踐,提供 初學者到中階開發者 都能直接套用的範例與說明,幫助你在開發 React+TS 時寫出更安全、更一致的程式碼。


核心概念

1️⃣ Function Component (FC) 的型別定義

React 官方提供了 React.FC<P>(或 React.FunctionComponent<P>)的泛型介面,讓我們在宣告函式元件時直接帶入 props 的型別

// 使用 React.FC 定義元件
import React from 'react';

interface GreetingProps {
  name: string;
  /** 可選的問候語,若未傳入則預設為 "Hello" */
  greeting?: string;
}

// 以 React.FC 包住 props 型別
const Greeting: React.FC<GreetingProps> = ({ name, greeting = 'Hello' }) => {
  return (
    <h1>{greeting}, {name}!</h1>
  );
};

export default Greeting;

重點React.FC 會自動為元件加入 children 的型別,如果不需要 children,可以改用 自訂函式型別

type SimpleComponent = (props: GreetingProps) => JSX.Element;

2️⃣ Props 型別的寫法與技巧

2.1 使用 interfacetype

方式 何時使用
interface 需要 擴充(extends)或 聲明合併
type 想要 交叉(&)或 聯合
// 例:使用 type 建立聯合型別的 props
type ButtonVariant = 'primary' | 'secondary' | 'danger';

type ButtonProps = {
  label: string;
  variant?: ButtonVariant; // 預設為 primary
  onClick: () => void;
};

2.2 預設值與可選屬性

const Button: React.FC<ButtonProps> = ({
  label,
  variant = 'primary',
  onClick,
}) => {
  const className = `btn btn-${variant}`;
  return (
    <button className={className} onClick={onClick}>
      {label}
    </button>
  );
};

2.3 Props 的泛型化(高階元件)

// HOC:將任何元件包裝成可接受 `className` 的元件
function withClassName<P extends object>(
  WrappedComponent: React.ComponentType<P>
) {
  return (props: P & { className?: string }) => {
    const { className, ...rest } = props;
    return <WrappedComponent {...(rest as P)} className={className} />;
  };
}

3️⃣ State 型別的正確寫法

3.1 useState 基本型別

import React, { useState } from 'react';

const Counter: React.FC = () => {
  // 明確指定 state 型別為 number
  const [count, setCount] = useState<number>(0);

  return (
    <div>
      <p>目前計數:{count}</p>
      <button onClick={() => setCount(c => c + 1)}>+1</button>
    </div>
  );
};

小技巧:若初始值已能推斷型別(如 useState(0)),可省略泛型;但在 nullundefined 為可能值時,一定要寫

const [selected, setSelected] = useState<string | null>(null);

3.2 複雜物件的 State

type Todo = {
  id: number;
  text: string;
  done: boolean;
};

const TodoList: React.FC = () => {
  const [todos, setTodos] = useState<Todo[]>([]);

  const toggle = (id: number) => {
    setTodos(prev =>
      prev.map(t => (t.id === id ? { ...t, done: !t.done } : t))
    );
  };

  return (
    <ul>
      {todos.map(t => (
        <li key={t.id}>
          <label>
            <input
              type="checkbox"
              checked={t.done}
              onChange={() => toggle(t.id)}
            />
            {t.text}
          </label>
        </li>
      ))}
    </ul>
  );
};

3.3 useReducer 的型別

type CounterAction =
  | { type: 'increment' }
  | { type: 'decrement' }
  | { type: 'reset'; payload: number };

function counterReducer(state: number, action: CounterAction): number {
  switch (action.type) {
    case 'increment':
      return state + 1;
    case 'decrement':
      return state - 1;
    case 'reset':
      return action.payload;
    default:
      return state;
  }
}

const CounterWithReducer: React.FC = () => {
  const [count, dispatch] = React.useReducer(counterReducer, 0);

  return (
    <div>
      <p>{count}</p>
      <button onClick={() => dispatch({ type: 'increment' })}>+</button>
      <button onClick={() => dispatch({ type: 'decrement' })}>-</button>
      <button onClick={() => dispatch({ type: 'reset', payload: 10 })}>
        Reset to 10
      </button>
    </div>
  );
};

常見陷阱與最佳實踐

陷阱 說明 解決方式
使用 React.FC 時自動加入 children 若元件不應接受 children,仍會允許傳入,可能造成不必要的錯誤。 改用 自訂函式型別const MyComp = (props: MyProps) => …
defaultProps 在 Function Component 中失效(TS 4.0+) defaultProps 只對 class component 有效,會導致型別推斷為 undefined 使用 參數解構賦值的預設值(如 ({ foo = 1 }))或 Partial?? 判斷。
any 逃脫型別檢查 隨意使用 any 會失去 TypeScript 的好處。 盡量使用 unknown具體型別,必要時再使用 as 斷言。
State 初始值與泛型不匹配 例如 useState([]) 會被推斷為 never[],導致後續 push 時出錯。 明確寫出泛型:useState<number[]>([])
Props 介面過於寬鬆 把所有屬性寫成 anyobject,失去自動完成與檢查。 使用 精確的屬性型別,必要時拆分為多個子介面。

最佳實踐

  1. 盡量避免 React.FC,除非真的需要 children
  2. 為每個元件建立獨立的 Props 型別,使用 interfacetype,保持可讀性。
  3. useStateuseReducer 中明確標註泛型,尤其是可能為 nullundefined 的情況。
  4. 使用 readonly 防止不小心改變 Props(例如 interface Props { readonly title: string; })。
  5. 利用 React.memo + 正確的 props 型別,避免不必要的重新渲染。

實際應用場景

🎯 Todo List(完整範例)

// TodoApp.tsx
import React, { useState } from 'react';

type Todo = {
  id: number;
  text: string;
  done: boolean;
};

type TodoFormProps = {
  onAdd: (text: string) => void;
};

const TodoForm: React.FC<TodoFormProps> = ({ onAdd }) => {
  const [value, setValue] = useState<string>('');

  const submit = (e: React.FormEvent) => {
    e.preventDefault();
    if (value.trim()) {
      onAdd(value.trim());
      setValue('');
    }
  };

  return (
    <form onSubmit={submit}>
      <input
        value={value}
        onChange={e => setValue(e.target.value)}
        placeholder="新增待辦"
      />
      <button type="submit">Add</button>
    </form>
  );
};

const TodoApp: React.FC = () => {
  const [todos, setTodos] = useState<Todo[]>([]);

  const addTodo = (text: string) => {
    const newTodo: Todo = {
      id: Date.now(),
      text,
      done: false,
    };
    setTodos(prev => [...prev, newTodo]);
  };

  const toggleTodo = (id: number) => {
    setTodos(prev =>
      prev.map(t => (t.id === id ? { ...t, done: !t.done } : t))
    );
  };

  return (
    <div>
      <h2>我的待辦清單</h2>
      <TodoForm onAdd={addTodo} />
      <ul>
        {todos.map(t => (
          <li key={t.id}>
            <label>
              <input
                type="checkbox"
                checked={t.done}
                onChange={() => toggleTodo(t.id)}
              />
              <span style={{ textDecoration: t.done ? 'line-through' : 'none' }}>
                {t.text}
              </span>
            </label>
          </li>
        ))}
      </ul>
    </div>
  );
};

export default TodoApp;
  • 型別優點TodoTodoFormPropsuseState<Todo[]> 全部明確,IDE 能即時提示錯誤與自動完成。
  • 實務意義:在團隊開發中,若有人不小心把 onAdd 寫成 onAdd: (num: number) => void,編譯器會直接報錯,避免了執行時的 undefined 行為。

🎯 表單驗證(使用 useReducer

type FormState = {
  email: string;
  password: string;
  errors: {
    email?: string;
    password?: string;
  };
};

type FormAction =
  | { type: 'SET_FIELD'; field: 'email' | 'password'; value: string }
  | { type: 'SET_ERROR'; field: 'email' | 'password'; message: string }
  | { type: 'CLEAR_ERRORS' };

function formReducer(state: FormState, action: FormAction): FormState {
  switch (action.type) {
    case 'SET_FIELD':
      return { ...state, [action.field]: action.value };
    case 'SET_ERROR':
      return {
        ...state,
        errors: { ...state.errors, [action.field]: action.message },
      };
    case 'CLEAR_ERRORS':
      return { ...state, errors: {} };
    default:
      return state;
  }
}

透過 嚴謹的 Action 型別,即使表單欄位增減,也能在編譯期捕捉錯誤,避免忘記處理某個欄位的驗證。


總結

  • FC、Props、State 是 React+TypeScript 中最常接觸的三大概念,正確的型別宣告能讓程式碼在 編譯階段即捕捉錯誤,提升開發效率。
  • React.FC 雖方便,但會自動加入 children,若不需要建議改用自訂函式型別。
  • Props 建議使用 interfacetype,配合 預設值可選屬性泛型,讓元件 API 更清晰。
  • StateuseStateuseReducer)必須明確標註泛型,特別是 null / undefined陣列物件 等複雜型別。
  • 常見陷阱包括 defaultPropsany、不恰當的 React.FC 使用,掌握最佳實踐後即可避免這些問題。

將本文的範例直接套用到自己的專案,配合 嚴謹的型別檢查,就能在實務開發中享受到 TypeScript 帶來的安全感與可維護性。祝你寫出 乾淨、可預測、且高品質 的 React+TypeScript 程式碼! 🚀