本文 AI 產出,尚未審核
React + TypeScript(實務應用)
FC / Props / State 型別
簡介
在 React 專案中加入 TypeScript,最直接的收益就是編譯期就能捕捉錯誤,提升程式碼可讀性與維護性。
然而,若只是把 .js 檔改成 .tsx,卻沒有正確為 Function Component (FC)、props、state 加上型別,仍然會遇到許多執行時的問題。
本篇文章聚焦在「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 使用 interface 或 type
| 方式 | 何時使用 |
|---|---|
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)),可省略泛型;但在 null 或 undefined 為可能值時,一定要寫:
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 介面過於寬鬆 | 把所有屬性寫成 any 或 object,失去自動完成與檢查。 |
使用 精確的屬性型別,必要時拆分為多個子介面。 |
最佳實踐:
- 盡量避免
React.FC,除非真的需要children。 - 為每個元件建立獨立的 Props 型別,使用
interface或type,保持可讀性。 - 在
useState、useReducer中明確標註泛型,尤其是可能為null、undefined的情況。 - 使用
readonly防止不小心改變 Props(例如interface Props { readonly title: string; })。 - 利用
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;
- 型別優點:
Todo、TodoFormProps、useState<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 建議使用
interface或type,配合 預設值、可選屬性 與 泛型,讓元件 API 更清晰。 - State(
useState、useReducer)必須明確標註泛型,特別是 null / undefined、陣列、物件 等複雜型別。 - 常見陷阱包括
defaultProps、any、不恰當的React.FC使用,掌握最佳實踐後即可避免這些問題。
將本文的範例直接套用到自己的專案,配合 嚴謹的型別檢查,就能在實務開發中享受到 TypeScript 帶來的安全感與可維護性。祝你寫出 乾淨、可預測、且高品質 的 React+TypeScript 程式碼! 🚀