TypeScript 課程:React + TypeScript(實務應用)— JSX 型別定義(JSX.Element)
簡介
在使用 React 開發前端介面時,JSX 已成為最常見的語法糖;而在 TypeScript 專案中,正確地描述 JSX 的型別是提升開發效率與避免執行時錯誤的關鍵。JSX.Element 是 TypeScript 為 React 元素所提供的基本型別,了解它的結構與使用方式,能讓我們在撰寫元件、傳遞子元素、或是建立高階組件(HOC)時得到完整的型別推斷與 IDE 提示。
本篇文章將從 概念說明、實作範例、常見陷阱,到 實務應用,一步步帶你掌握 JSX.Element 的正確寫法,讓你的 React + TypeScript 專案更安全、更可維護。
核心概念
1. JSX.Element 是什麼?
JSX.Element代表 React 元素,亦即React.createElement(...)的回傳值。- 在 TypeScript 中,
JSX.Element的定義大致如下(簡化版):
type JSXElement = {
type: string | FunctionComponent<any> | ComponentClass<any>;
props: any;
key: string | number | null;
};
- 注意:
JSX.Element不等同於ReactNode。ReactNode包含了JSX.Element、string、number、null、undefined、boolean以及陣列等更廣義的型別。 - 因此,在只允許傳入「真正的 React 元素」的情境下,我們會使用
JSX.Element;而在允許文字、陣列等多元輸出的情況下,則使用ReactNode。
2. 為什麼要明確宣告 JSX.Element?
- 型別安全:避免把非 React 元素(例如
string)傳給只接受元件的函式。 - IDE 補全:正確的型別讓 VSCode、WebStorm 等編輯器提供屬性提示與錯誤檢查。
- 文件化:在專案文件或元件 API 說明中,使用
JSX.Element能清楚表達「此參數必須是 React 元素」的意圖。
3. 常見的型別宣告方式
| 場景 | 型別 | 範例 |
|---|---|---|
元件的 children 必須是單一元素 |
JSX.Element |
children: JSX.Element |
| 元件接受任意子節點(文字、陣列…) | ReactNode |
children: React.ReactNode |
| 高階組件返回的元件類型 | JSX.Element |
function withLogging<P>(Component: React.ComponentType<P>): React.FC<P> |
| 直接在函式中回傳 JSX | JSX.Element |
function renderHeader(): JSX.Element |
程式碼範例
以下範例示範在不同情境下如何使用 JSX.Element,並附上說明註解。
範例 1:單一子元素的 children 型別
import React from 'react';
type CardProps = {
/** 必須傳入一個 React 元素作為卡片內容 */
children: JSX.Element;
};
const Card: React.FC<CardProps> = ({ children }) => {
return (
<div className="card">
{/* 直接渲染傳入的 JSX.Element */}
{children}
</div>
);
};
// 使用範例
<Card>
{/* 這裡必須是單一的 JSX.Element,不能是文字或陣列 */}
<h2>卡片標題</h2>
</Card>;
說明:若 children 被宣告為 JSX.Element,傳入 <Card> 時只能提供單一的 React 元素;若想允許文字或多個子元素,應改用 ReactNode。
範例 2:函式回傳 JSX.Element
/**
* 產生一個簡易的訊息提示元件
* @param message 要顯示的文字
* @returns JSX.Element
*/
function Alert(message: string): JSX.Element {
return (
<div className="alert">
<strong>注意:</strong> {message}
</div>
);
}
// 在其他元件中使用
const App: React.FC = () => {
return (
<section>
{Alert('系統維護中,請稍後再試。')}
</section>
);
};
說明:將 Alert 定義為回傳 JSX.Element,讓 TypeScript 知道此函式永遠會產生一個 React 元素,避免把 void 或 string 誤傳。
範例 3:高階組件(HOC)返回 JSX.Element
import React from 'react';
/**
* 為傳入的元件加上 console.log 的 HOC
* @param WrappedComponent 任意 React 元件
*/
function withLogger<P>(WrappedComponent: React.ComponentType<P>) {
const ComponentWithLogger: React.FC<P> = (props) => {
console.log('Render with props:', props);
// 使用 React.createElement 仍會得到 JSX.Element
const element = <WrappedComponent {...props} />;
return element; // 型別為 JSX.Element
};
// 為了讓外部知道回傳的是元件,使用 React.FC<P>
return ComponentWithLogger;
}
// 原始元件
const Hello: React.FC<{ name: string }> = ({ name }) => <p>Hello, {name}!</p>;
// 包裝後的元件
const HelloWithLog = withLogger(Hello);
// 使用
<HelloWithLog name="Alice" />;
說明:withLogger 內部建立的 element 型別自動推斷為 JSX.Element。對外回傳的仍是 React 元件,但在實作時,我們可以安心使用 JSX.Element 作為暫存或回傳值。
範例 4:條件渲染與 JSX.Element | null
type ConditionalProps = {
show: boolean;
};
const ConditionalRender: React.FC<ConditionalProps> = ({ show }) => {
// 依照條件回傳 JSX.Element 或 null
const content: JSX.Element | null = show ? (
<p>顯示的內容</p>
) : null;
return <section>{content}</section>;
};
說明:在條件渲染時,常見的型別是 JSX.Element | null,因為 null 代表「什麼都不渲染」。若只寫 JSX.Element,TS 會報錯。
範例 5:使用 React.cloneElement 時的型別
type CloneProps = {
/** 必須是可被 clone 的 React 元素 */
element: JSX.Element;
};
const CloneWrapper: React.FC<CloneProps> = ({ element }) => {
// cloneElement 會回傳同樣的 JSX.Element
const cloned = React.cloneElement(element, { className: 'cloned' });
return cloned; // 型別仍為 JSX.Element
};
說明:React.cloneElement 的回傳型別為 React.ReactElement<any, any>,它與 JSX.Element 兼容,可直接作為 JSX 渲染。
常見陷阱與最佳實踐
| 陷阱 | 說明 | 解決方式 |
|---|---|---|
把 ReactNode 當 JSX.Element 使用 |
ReactNode 包含 string、number,直接指定 JSX.Element 會導致型別錯誤。 |
若允許文字或陣列,改用 React.ReactNode;若只能是元素,保留 JSX.Element。 |
忘記 null 或 undefined 的型別 |
條件渲染常回傳 null,但型別只寫 JSX.Element 會產生錯誤。 |
使用聯合型別 `JSX.Element |
在泛型元件中未正確傳遞 JSX.Element |
泛型若寫成 T extends JSX.Element,實際傳入 null 會失敗。 |
只在需要限制為「一定是元素」時使用;否則使用 ReactNode。 |
過度使用 any 失去型別保護 |
為了快速解決錯誤,直接把參數改成 any。 |
盡量使用具體型別,如 JSX.Element、ReactElement<any, any>,或使用 React.ComponentPropsWithoutRef<typeof Component> 取得正確屬性型別。 |
忘記 key 的型別 |
在陣列渲染時,key 必須是 `string |
number,但 JSX.Element本身不保證有key`。 |
最佳實踐
- 明確區分
JSX.Element與ReactNode:只在「必須是 React 元素」的 API 中使用JSX.Element。 - 條件渲染時加入
null:const result: JSX.Element | null = condition ? <Comp/> : null;。 - 在自訂 Hook 中返回 JSX:若 Hook 需要返回 UI,使用
JSX.Element而非any。 - 使用
React.FC或React.ComponentType搭配泛型,讓傳入的子元素型別自動推斷。 - 利用
React.cloneElement或React.isValidElement做安全檢查,避免把非元素傳入需要JSX.Element的函式。
實際應用場景
1. 建立 UI 套件庫(Component Library)
在開發像是 Material‑UI、Ant Design 這類套件時,元件的 children 必須是 合法的 React 元素,才能保證樣式與行為的正確性。此時,對外 API 常會寫成:
export interface ModalProps {
/** 僅接受單一根元素作為內容 */
children: JSX.Element;
}
這樣使用者若誤傳入文字或陣列,編譯階段就會直接報錯,提升套件的健全度。
2. 高階組件(HOC)與 Render Props
在實作 權限檢查 HOC 或 資料抓取 Render Props 時,我們常需要把子元素作為參數傳入,並在內部返回新元素:
function withAuth<P>(Component: React.ComponentType<P>) {
return (props: P): JSX.Element => {
const isLoggedIn = useAuth();
return isLoggedIn ? <Component {...props} /> : <Redirect to="/login" />;
};
}
return 的型別明確為 JSX.Element,讓 TypeScript 能正確推斷 withAuth 的回傳型別。
3. 自訂 Hook 回傳 UI
有時我們會把 UI 抽成 Hook,例如 useLoadingOverlay:
function useLoadingOverlay(isLoading: boolean): JSX.Element | null {
return isLoading ? (
<div className="overlay">Loading...</div>
) : null;
}
使用者在組件中直接寫 {useLoadingOverlay(isFetching)},型別安全且可讀性佳。
4. 渲染函式(Render Function)作為 Prop
type ListProps<T> = {
items: T[];
/** 渲染每一筆資料的函式,必須回傳 JSX.Element */
renderItem: (item: T, index: number) => JSX.Element;
};
function List<T>({ items, renderItem }: ListProps<T>) {
return (
<ul>
{items.map((it, i) => (
<li key={i}>{renderItem(it, i)}</li>
))}
</ul>
);
}
renderItem 的回傳型別使用 JSX.Element,確保每筆資料都會產生合法的 React 元素。
總結
JSX.Element是 React 元素的型別,專門用來描述「必須是 JSX」的情境。- 正確區分
JSX.Element與ReactNode,能避免許多型別衝突與執行時錯誤。 - 在 單一子元素、函式回傳、HOC、Render Props 等常見模式中,使用
JSX.Element能獲得最佳的型別安全與 IDE 支援。 - 留意 條件渲染、null/undefined、以及
any的濫用,遵循「盡可能具體」的原則。 - 在實務開發(套件庫、權限檢查、Hook、Render Function)中,將
JSX.Element作為 API 的型別定義,是提升程式碼可讀性、可維護性與錯誤預防的關鍵。
掌握了 JSX.Element 的概念與寫法後,你的 React + TypeScript 專案將會變得更安全、可預測,也能在團隊協作時提供更清晰的型別契約。祝你在實務開發中玩得開心,寫出高品質的 TypeScript React 程式碼!