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

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

































