React 與 TypeScript:提升代碼質量的十個模式
構建可擴展且可維護的 React 應用常面臨諸多挑戰,包括類型安全性缺失、項目膨脹帶來的維護難題、不可靠的屬性驗證以及脆弱的 DOM 操作等。雖然普通 JavaScript 能解決大部分問題,但它缺乏為代碼庫提供長期保障的安全機制。這正是 TypeScript 的價值所在——它能以一致且可擴展的方式解決這些反復出現的問題。
本文將探討若干經過驗證的模式,幫助您在 React 和 TypeScript 中編寫更安全、更清晰且更易讀的代碼。
TypeScript 在 React 中的優勢
TypeScript 為 React 應用帶來多重優勢,既能提升代碼質量,又能提高開發效率:
- 可維護性:使代碼更具可讀性和自解釋性,助力團隊高效管理和擴展項目
- 早期錯誤檢測:在編譯階段識別錯誤,讓開發者能在問題影響終端用戶前及時修復
- 更佳工具支持:提供卓越的 IDE 支持,包括自動補全、重構和代碼導航等功能,優化開發體驗
- 類型安全:在開發過程中捕獲類型相關錯誤,減少運行時錯誤,提升代碼可靠性
- 重構信心:通過即時標記錯誤的類型使用,確保代碼變更更安全
類型化組件屬性與默認屬性
在 TypeScript 中,接口非常適合描述組件屬性,特別是在需要多處擴展或實現時。以下展示如何通過接口聲明和使用屬性:
import Reactfrom'react';
interfaceMyEmployeeProps {
name: string;
age: number;
isEmployed?: boolean; // 可選屬性
}
constMyEmployee: React.FC<MyEmployeeProps> = ({ name, age, isEmployed }) => {
return (
<div>
<p>姓名: {name}</p>
<p>年齡: {age}</p>
{isEmployed !== undefined && <p>雇傭狀態: {isEmployed ? '是' : '否'}</p>}
</div>
);
};當需要組合聯合類型或交叉類型時,可用 type 替代 interface,但出于可擴展性考慮,通常更推薦使用 interface:
import Reactfrom'react';
typeSubmitButtonProps = {
text: string;
onClick: () =>void;
variant?: 'primary' | 'secondary'; // 聯合類型
};
constUserButton: React.FC<SubmitButtonProps> = ({ text, onClick, variant }) => {
return (
<button
onClick={onClick}
className={variant === 'primary' ? 'primary-button' : 'secondary-button'}
>
{text}
</button>
);
};在 TypeScript 與 React 結合使用時,組件屬性默認視為必填,除非添加 ? 標記為可選。無論使用接口還是類型別名描述屬性,此規則均適用。
必填屬性示例:
interface MyEmployeeProps {
requiredFullName: string;
requiredAge: number;
}
const MyEmployee: React.FC<MyEmployeeProps> = ({ requiredFullName, requiredAge }) => {
return (
<div>
{requiredFullName} {requiredAge}
</div>
);
};可選屬性示例:
interface MyEmployeeProps {
requiredFullName: string;
optionalAge?: number;
}
const MyEmployee: React.FC<MyEmployeeProps> = ({ requiredFullName, optionalAge }) => {
return (
<div>
{requiredFullName} {optionalAge}
</div>
);
};默認屬性與函數組件參數默認值:
// 類組件
classUserComponentextendsReact.Component<UserProps> {
render(){
return (
<div style={{ color: this.props.color, fontSize: this.props.fontSize}}>
{this.props.title}
</div>
);
}
}
UserComponent.defaultProps = {
color: 'blue'
fontSize: 20,
};
// 函數組件
constUserFunctionalComponent: React.FC<UserProps> = ({
title,
color = "blue",
fontSize = 20
}) => {
return<div style={{ color: color, fontSize: fontSize }}>{title}</div>;
};通過類組件的 defaultProps 屬性,您可以為屬性設置默認值,確保即使某些屬性未提供時組件行為仍可預測。而在函數組件中,只需直接在函數參數中為可選屬性分配默認值即可。這種方式不僅使代碼更簡潔,還能有效防止因缺失屬性導致的運行時錯誤。
處理子元素:
interface UserComponentProps {
title: string;
children: React.ReactNode;
}
const UserComponent: React.FC<UserComponentProps> = ({ title, children }) => {
return (
<div>
<h1>{title}</h1>
{children}
</div>
);
};如上所示,children 屬性允許您傳遞文本、其他組件甚至多個元素等廣泛數據類型的內容,使組件通過"包裹"或顯示您放入其中的任何內容而變得更靈活和可復用。
使用可辨識聯合進行條件渲染
什么是可辨識聯合?何時使用?
當您使用 TypeScript 和 React 構建應用時,經常需要處理可能處于不同狀態的單一數據:加載中、錯誤或成功。可辨識聯合(有時稱為標記聯合或代數數據類型)為建模這些不同形式提供了整潔的方式。通過將相關類型分組到一個標簽下,您可以在保持類型安全的同時減輕編碼時的思維負擔。
這種清晰的分離使得在組件中決定顯示哪個 UI 變得簡單,因為每個狀態都帶有自己的特征。在以下示例中,我們將看到這種方法如何幫助我們編寫更安全、更可讀且仍具表現力的代碼:
type DataLoadingState = {
status: 'request loading...';
};
typeDataSuccessState<T> = {
status: 'request success';
data: T;
};
typeDataErrorState = {
status: 'request error';
message: string;
};
typeDataState<T> = DataLoadingState | DataSuccessState<T> | DataErrorState;從上述代碼片段可見,每種類型都有一個共同特征(通常稱為判別器或標記)來標識其種類,類似于狀態標簽。當這些形狀被合并為聯合類型時,TypeScript 依賴此標記來區分它們。由于每種形狀對該特征都有不同的固定值,語言能準確知道當前是哪種類型并相應縮小類型范圍。一旦定義了這些形狀,您就可以用 | 操作符將它們捆綁在一起,從而以保持安全且可預測的方式對復雜狀態進行建模。
使用 never 類型進行窮盡檢查
TypeScript 中通過 never 類型進行窮盡檢查是一種技術,可確保在 switch 語句或條件邏輯中顯式處理可辨識聯合的所有可能情況,使開發者能通過類型安全在編譯時捕獲未處理的場景。
值得注意的是,never 類型表示永遠不會出現的值(即不可達代碼),用于窮盡檢查以確保正確處理可辨識聯合的所有情況。如果添加了新情況但未處理,編譯器將拋出錯誤,從而增強類型安全:
function DisplayData<T>({ state }: { state: DataState<T> }) {
switch (state.status) {
case'loading':
return<p>數據加載中</p>;
case'success':
return<p>數據: {JSON.stringify(state.data)}</p>;
case'error':
return<p>錯誤: {state.message}</p>;
default:
return<p>未知狀態</p>;
}
}上述代碼展示了在 React 組件中有效使用可辨識聯合的最后一步——基于判別屬性(status)使用 switch 或 if 語句等條件邏輯。這將允許您根據當前狀態渲染不同的 UI 元素,并在編譯時捕獲缺失的分支,保持組件既類型安全又抗錯誤。
使用 ReturnType 和 typeof 從 API 推斷類型
TypeScript 提供了兩個強大的實用工具:typeof 和 ReturnType<T>,分別用于從現有值推斷類型和提取函數的返回類型,特別是在處理服務、API 和實用函數時,能實現更安全且更易維護的代碼。
使用 typeof 從函數或常量推斷類型
對于常量,typeof 用于推斷變量(字符串)的類型,使其可復用而無需硬編碼,如下所示:
const API_BASE_URL = 'https://api.newpayment.com/services/api/v1/transfer';
type ApiBaseUrlType = typeof API_BASE_URL;您也可以使用 typeof 獲取函數類型,這對類型化回調很有用:
const getEmployeeDetails = (employeeId: number) => ({
employeeId,
employeeName: 'Peter Aideloje',
employeeEmail: 'aidelojepeter123@gmail.com',
position: 'Software Engineer',
});
// 使用 typeof 獲取函數類型
type GetEmployeeDetailsFnType = typeof getEmployeeDetails;利用 ReturnType<T> 獲取函數結果
當實用/服務函數返回結構化數據時,此模式非常有用。通過 ReturnType 自動派生結果類型,確保代碼庫中的一致性。結合 ReturnType 和 typeof,可使類型與函數簽名保持同步,避免手動重復并降低類型不匹配的風險:
// 獲取 getUser 函數的返回類型
const employeeDetails: EmployeeDetails = {
employeeId = 3,
employeeName: 'Peter Aideloje',
employeeEmail: 'aidelojepeter123@gmail.com',
position: 'Software Engineer',
};
type EmployeeDetails = ReturnType<typeof getEmployeeDetails>;從服務和實用函數提取類型
這有助于從實用或服務函數的結構化數據中自動派生結果類型,從而確保消費組件的一致性,如下所示:
// 實用函數
functioncalculateTotalFee(price: number, quantity: number) {
return {
total: price * quantity,
currency: 'GBP',
};
}
// 提取實用函數的返回類型
typeTotalSummary = ReturnType<typeof calculateTotalFee>;
constsummary: TotalSummary = {
total: 100,
currency: 'GBP',
};實用類型:Pick、Omit、Partial、Record
TypeScript 提供了一組內置實用類型,可靈活地從已定義的類型構建新類型。這些工具能幫助塑造組件屬性、組織狀態、減少冗余并提升 React 項目的代碼可維護性。以下是 React + TypeScript 設置中最常用實用類型的實際用例。
各實用類型的實際用例
- Pick<Type, Keys>
Pick 實用類型通過從大型 Type 中選擇特定屬性來構造新類型,從而增強類型安全并減少冗余:
interface Employee {
employeeId: number;
employeeName: String;
employeeEmail: String;
employeePosition: String;
}
typeEmployeePreview = Pick<Employee, 'employeeId' | 'employeeName'>;
constpreview: Employeepreview = {
employeeId: 35,
employeeName: 'Peter Aideloje',
};這非常適合在列表或組件中顯示最小數據量。
- Omit<Type, Keys>
Omit 實用類型與 Pick 直接相反,用于通過排除現有類型中的特定屬性來創建新類型:
interface Employee {
employeeId: number;
employeeName: String;
employeeEmail: String;
employeePosition: String;
}
typeEmployeeWithoutEmail = Omit<Employee, 'employeeEmail'>;
constemployee: EmployeeWithoutEmail = {
employeeId: 35,
employeeName: 'Peter Aideloje',
employeePosition: 'Software Engineer',
};這非常適合排除不必要的信息或敏感字段,如密碼、電子郵件或數據庫 ID。
- Partial<Type>
Partial 實用類型使類型中的所有屬性變為可選。這在更新對象且不需要提供所有屬性時非常有用:
interface Employee {
employeeId: number;
employeeName: String;
employeeEmail: String;
employeePosition: String;
}
typePartialEmployee = Partial<Employee>;
constpartialEmployee: PartialEmployee = {
employeeName: 'Peter Aideloje',
};- Record<Keys, Type>
Record 實用類型創建具有特定鍵集和類型的對象:
type Roles = "admin" | "employee" | "viewer";
type Permissions = Record<Role, string[]>;
const permissions: Permissions = {
admin["read", "write", "delete"],
employee["read", "write"],
viewer["read"],
};TypeScript 中的實用類型通過重用和重塑現有類型,在定義屬性或狀態時有助于減少代碼重復。它們也非常適合建模靈活的數據結構,如動態表單輸入或 API 響應,使代碼庫更清晰且更易于維護。
泛型組件與鉤子
使用泛型編寫可復用組件
TypeScript 中的泛型幫助開發者創建可管理多種數據類型的可復用 UI 元素,同時保持強大的類型安全。在 React 中設計不綁定特定數據類型的組件時,它們表現更出色且更重要。這種靈活性使您的 React 組件更具動態性,并能適應應用程序任何部分所需的各種類型。要實現這一點,請按照以下步驟設置您的項目:
首先,打開終端或命令提示符運行命令以使用 TypeScript 創建新的 React 項目:
npx create-react-app react-project --template typescript接下來,此命令將導航到項目目錄:
cd react-project文件夾結構:

接下來,我們將創建一個通用的 List 組件,可以使用以下代碼片段展示任何類型的項目列表:
import Reactfrom'react';
// 泛型組件
typeProps<T> = {
items: T[];
renderItem: (item: T) =>React.ReactNode;
};
functionGenericComponent<T>({ items, renderItem }: Props<T>): JSX.Element {
return<div>{items.map(renderItem)}</div>;
}
exportdefaultGenericComponent;GenericComponent 在 React + TypeScript 設置中定義了一個可復用的泛型列表組件。它接受兩個屬性:一個項目數組和一個 renderItem 函數,該函數決定如何顯示每個項目。泛型的使用使該組件能夠處理任何數據類型,使其成為跨多種用例渲染列表的更靈活且類型安全的解決方案。
類型化引用和 DOM 元素
- 將 useRef 與 DOM 元素結合使用
在 React 開發中,有必要利用庫提供的 useRef 等內置工具。當將 useRef 與 HTMLInputElement 等 DOM 元素結合使用時,您需要如下指定引用:
import React, { useRef, useEffect } from'react';
constFocusInput: React.FC = () => {
const nameInputRef = useRef<HTMLInputElement | null>(null);
useEffect(() => {
nameInputRef.current?.focus();
}, []);
return (
<div>
<label htmlFor='name'>姓名:</label>
<input id='name' type='text' ref={nameInputRef} />
</div>
);
};
exportdefaultFocusInput;- 使用 React.forwardRef 轉發引用
在 React 中,forwardRef 是一個方便的功能,允許您將引用從父組件傳遞到子組件。當子組件包裝了 DOM 元素但不直接暴露它時,這非常有用。本質上,React.forwardRef 允許父組件直接訪問內部 DOM 節點(子組件的 DOM),即使它被隱藏或包裝在其他抽象層中。在使用 TypeScript 時,您需要定義引用的類型以保持安全性和可預測性。這是使組件更靈活且更易維護的好方法:
import React, { forwardRef, useRef, useImperativeHandle } from'react';
typeButtonProps = {
handleClick?: () =>void;
};
constCustomerButton = forwardRef<HTMLButtonElement, ButtonProps>((props, ref) => {
const internalRef = useRef<HTMLButtonElement>(null);
useImperativeHandle(ref, () => ({
focus: () => {
internalRef.current?.focus();
},
}));
return (
<button ref={internalRef} onClick={props.hanldeClick}>
點擊這里
</button>
);
});
constWrapperComponent = () => {
const refToButton = useRef<HTMLButtonElement>(null);
consttriggerFocus = () => {
refToButton.current?.focus();
};
return (
<div>
<customButton ref={refToButton} handleClick={triggerFocus} />
</div>
);
};
exportdefaultWrapperComponent;- 避免任何 DOM 操作
在 React 中,盡量避免直接修改 DOM。相反,采用更可靠且可維護的方法,使用 React 的內置狀態系統來管理變更。例如,與其使用引用來手動設置輸入字段的值,不如讓 React 通過狀態控制它。這使您的組件更可預測且更易于調試:
import React, { useState, useRef, useEffect } from'react';
functionControlledInput() {
const [inputValue, setInputValue] = useState('');
const inputRef = useRef<HTMLInputElement>(null);
consthandleInputChange = (event: React.ChangeEvent<HTMLInputElement>) => {
setInputValue(event.target.value);
};
useEffect(() => {
if (inputRef.current) {
//安全訪問屬性
console.log(inputRef.current.value);
// 不要直接操作 DOM,改用 React 狀態
}
}, [inputValue]);
return<input type='text' ref={inputRef} value={inputValue} onChange={handleInputChange} />;
}強類型化的 Context
使用泛型類型創建和消費 Context
當您使用 React 和 TypeScript 構建應用時,createContext 方法允許您將主題偏好或登錄用戶詳情等內容傳遞到遠距離組件,而無需通過每一層傳遞屬性。為了保持此過程類型安全且易于管理,首先編寫一個 TypeScript 類型或接口,明確列出 Context 將保存的每項數據。這樣做能讓編譯器及早標記錯誤,并在導入 Context 的任何地方保持其形狀一致。
定義好類型后,向 React.createContext 傳遞合理的默認值并將該值作為參數提供。默認值確保任何在 Provider 外部讀取 Context 的組件都能獲得安全回退,而非導致應用崩潰。React 16 引入的 Context API 已成為以更清晰、更可擴展的方式全局共享狀態的首選方法。下面,我們將通過三個簡單步驟創建 Context、提供它,然后在組件中消費它。
- 用接口定義 Context
interface AppContextType{
currentValue: string;
updateValue(updated: string) => void;
}- 創建 Context
import React from 'react';
const AppContext = React.createContext<AppContextType>({
currentValue: 'default',
updateValue: () => {}, //臨時函數占位符
});- 消費 Context
import React, { useContext } from'react';
import { AppContext } from'./AppContextProvider'; //假設 Context 定義在單獨文件中
functioninfoDisplay() {
const { currentValue, updateValue } = useContext(AppContext);
return (
<section>
<p>當前 Context: {currentValue}</p>
<button onClick={() => updateValue('updateContext')}>更改值</button>
</section>
);
}將 createContext 與默認值和未定義檢查結合使用
在 React + TypeScript 設置中使用 createContext 時,必須注意定義默認值并處理 Context 可能為 undefined 的情況。這將幫助您確保應用保持安全、可預測且不易出現運行時錯誤。
- createContext 中的默認值
在 React 中調用 createContext 時,您可以傳遞默認值作為參數。當讀取 Context 的組件不在正確的 Provider 內,或 Provider 本身將值設為 undefined 時,useContext 會返回該值:
interface IThemeContext {
theme: 'light' | 'dark';
switchTheme: () => void;
}
const ThemeContext = React.createContext<IThemeContext | null>(null);- 使用 useContext 處理未定義
當您用 React 的 useContext Hook 拉取數據但忘記將組件包裝在匹配的 Provider 中,或該 Provider 意外發送 undefined 時,Hook 只會返回 undefined。為了讓 TypeScript 滿意并為應用提供防止隱蔽運行時錯誤的安全網,在讀取 Context 后始終添加快速檢查。這樣,當 Context 缺失時,您的組件能冷靜應對而非崩潰:
import { createContext, useContext } from'react';
interfaceContextShape {
data: string;
}
const customContext = createContext<ContextShape | undefined>(undefined);
exportfunctionuseCustomContext() {
const ctx = useContext(CustomContext);
if (!ctx) {
thrownewError('useCustomContext 必須在 customProvider 內使用');
}
return ctx;
}
exportfunctionCustomProvider({ children }: { children: React.ReactNode }) {
constcontextValue: contextShape = { data: '共享 Context 數據' };
return<CustomContext.Provider value={contextValue}>{children}</CustomContext.Provider>;
}結論
我們已經看到 TypeScript 在現代 React 開發中發揮的關鍵作用,它幫助團隊構建更具可擴展性、健壯性和可維護性的應用,同時提高代碼可讀性。開發者可以使用 typeof、ReturnType等特性從 API 推斷類型,從而減少手動重復并保持類型與實際實現同步。此外,當您在代碼庫中啟用類型化組件屬性和默認屬性時,可以及早捕獲誤用并提高代碼可讀性,如本文所示。
TypeScript 在處理類型化引用和 DOM 元素等底層關注點,以及在 React Context 中實現強類型化以使消費組件更清晰安全方面也表現出色。
如果您不熟悉這些模式,不必急于一次性全部采用。在 React 中采用 TypeScript 不必令人望而生畏;您可以從在能立即帶來價值的地方小規模引入開始,然后逐步擴展。隨著時間的推移,這些實踐將成為第二天性,并在可維護性、代碼質量和投資回報方面帶來長期收益。
編碼愉快!
原文地址:https://blog.logrocket.com/react-typescript-10-patterns-writing-better-code/作者:Peter Aideloje

































