构建可扩展且可维护的 React 应用常面临诸多挑战,包括类型安全性缺失、项目膨胀带来的维护难题、不可靠的属性验证以及脆弱的 DOM 操作等。虽然普通 JavaScript 能解决大部分问题,但它缺乏为代码库提供长期保障的安全机制。这正是 TypeScript 的价值所在——它能以一致且可扩展的方式解决这些反复出现的问题。
本文将探讨若干经过验证的模式,帮助您在 React 和 TypeScript 中编写更安全、更清晰且更易读的代码。
TypeScript 在 React 中的优势 TypeScript 为 React 应用带来多重优势,既能提升代码质量,又能提高开发效率:
可维护性 :使代码更具可读性和自解释性,助力团队高效管理和扩展项目 早期错误检测 :在编译阶段识别错误,让开发者能在问题影响终端用户前及时修复 更佳工具支持 :提供卓越的 IDE 支持,包括自动补全、重构和代码导航等功能,优化开发体验 类型安全 :在开发过程中捕获类型相关错误,减少运行时错误,提升代码可靠性 重构信心 :通过即时标记错误的类型使用,确保代码变更更安全 类型化组件属性与默认属性 在 TypeScript 中,接口非常适合描述组件属性,特别是在需要多处扩展或实现时。以下展示如何通过接口声明和使用属性:
import React from 'react' ; interface MyEmployeeProps { name : string ; age : number ; isEmployed ?: boolean ; // 可选属性 } const MyEmployee : React . FC < MyEmployeeProps > = ( { name, age, isEmployed } ) => { return ( < div > < p > 姓名: {name} </ p > < p > 年龄: {age} </ p > {isEmployed !== undefined && < p > 雇佣状态: {isEmployed ? '是' : '否'} </ p > } </ div > ); };
当需要组合联合类型或交叉类型时,可用 type
替代 interface
,但出于可扩展性考虑,通常更推荐使用 interface
:
import React from 'react' ; type SubmitButtonProps = { text : string ; onClick : () => void ; variant ?: 'primary' | 'secondary' ; // 联合类型 }; const UserButton : 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 > ); };
默认属性与函数组件参数默认值 :
// 类组件 class UserComponent extends React.Component < UserProps > { render (){ return ( < div style = {{ color: this.props.color , fontSize: this.props.fontSize }}> {this.props.title} </ div > ); } } UserComponent . defaultProps = { color : 'blue' fontSize : 20 , }; // 函数组件 const UserFunctionalComponent : 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...' ; }; type DataSuccessState <T> = { status : 'request success' ; data : T; }; type DataErrorState = { status : 'request error' ; message : string ; }; type DataState <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>;
从服务和实用函数提取类型 这有助于从实用或服务函数的结构化数据中自动派生结果类型,从而确保消费组件的一致性,如下所示:
// 实用函数 function calculateTotalFee ( price : number , quantity : number ) { return { total : price * quantity, currency : 'GBP' , }; } // 提取实用函数的返回类型 type TotalSummary = ReturnType < typeof calculateTotalFee>; const summary : TotalSummary = { total : 100 , currency : 'GBP' , };
实用类型: Pick
、 Omit
、 Partial
、 Record
TypeScript 提供了一组内置实用类型,可灵活地从已定义的类型构建新类型。这些工具能帮助塑造组件属性、组织状态、减少冗余并提升 React 项目的代码可维护性。以下是 React + TypeScript 设置中最常用实用类型的实际用例。
各实用类型的实际用例 Pick
实用类型通过从大型 Type
中选择特定属性来构造新类型,从而增强类型安全并减少冗余:
interface Employee { employeeId : number ; employeeName : String ; employeeEmail : String ; employeePosition : String ; } type EmployeePreview = Pick < Employee , 'employeeId' | 'employeeName' >; const preview : Employeepreview = { employeeId : 35 , employeeName : 'Peter Aideloje' , };
这非常适合在列表或组件中显示最小数据量。
Omit
实用类型与 Pick
直接相反,用于通过排除现有类型中的特定属性来创建新类型:
interface Employee { employeeId : number ; employeeName : String ; employeeEmail : String ; employeePosition : String ; } type EmployeeWithoutEmail = Omit < Employee , 'employeeEmail' >; const employee : EmployeeWithoutEmail = { employeeId : 35 , employeeName : 'Peter Aideloje' , employeePosition : 'Software Engineer' , };
这非常适合排除不必要的信息或敏感字段,如密码、电子邮件或数据库 ID。
Partial
实用类型使类型中的所有属性变为可选。这在更新对象且不需要提供所有属性时非常有用:
interface Employee { employeeId : number ; employeeName : String ; employeeEmail : String ; employeePosition : String ; } type PartialEmployee = Partial < Employee >; const partialEmployee : PartialEmployee = { employeeName : 'Peter Aideloje' , };
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 React from 'react' ; // 泛型组件 type Props <T> = { items : T[]; renderItem : ( item : T ) => React . ReactNode ; }; function GenericComponent <T>({ items, renderItem }: Props <T>): JSX . Element { return < div > {items.map(renderItem)} </ div > ; } export default GenericComponent ;
GenericComponent
在 React + TypeScript 设置中定义了一个可复用的泛型列表组件。它接受两个属性:一个项目数组和一个 renderItem
函数,该函数决定如何显示每个项目。泛型的使用使该组件能够处理任何数据类型,使其成为跨多种用例渲染列表的更灵活且类型安全的解决方案。
类型化引用和 DOM 元素 在 React 开发中,有必要利用库提供的 useRef
等内置工具。当将 useRef
与 HTMLInputElement
等 DOM 元素结合使用时,您需要如下指定引用:
import React , { useRef, useEffect } from 'react' ; const FocusInput : 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 > ); }; export default FocusInput ;
在 React 中, forwardRef
是一个方便的功能,允许您将引用从父组件传递到子组件。当子组件包装了 DOM 元素但不直接暴露它时,这非常有用。本质上, React.forwardRef
允许父组件直接访问内部 DOM 节点(子组件的 DOM),即使它被隐藏或包装在其他抽象层中。在使用 TypeScript 时,您需要定义引用的类型以保持安全性和可预测性。这是使组件更灵活且更易维护的好方法:
import React , { forwardRef, useRef, useImperativeHandle } from 'react' ; type ButtonProps = { handleClick ?: () => void ; }; const CustomerButton = forwardRef< HTMLButtonElement , ButtonProps >( ( props, ref ) => { const internalRef = useRef< HTMLButtonElement >( null ); useImperativeHandle (ref, () => ({ focus : () => { internalRef. current ?. focus (); }, })); return ( < button ref = {internalRef} onClick = {props.hanldeClick} > 点击这里 </ button > ); }); const WrapperComponent = () => { const refToButton = useRef< HTMLButtonElement >( null ); const triggerFocus = () => { refToButton. current ?. focus (); }; return ( < div > < customButton ref = {refToButton} handleClick = {triggerFocus} /> </ div > ); }; export default WrapperComponent ;
在 React 中,尽量避免直接修改 DOM。相反,采用更可靠且可维护的方法,使用 React 的内置状态系统来管理变更。例如,与其使用引用来手动设置输入字段的值,不如让 React 通过状态控制它。这使您的组件更可预测且更易于调试:
import React , { useState, useRef, useEffect } from 'react' ; function ControlledInput () { const [inputValue, setInputValue] = useState ( '' ); const inputRef = useRef< HTMLInputElement >( null ); const handleInputChange = ( 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、提供它,然后在组件中消费它。
interface AppContextType { currentValue : string ; updateValue ( updated : string ) => void ; }
创建 Context import React from 'react' ; const AppContext = React . createContext < AppContextType >({ currentValue : 'default' , updateValue : () => {}, //临时函数占位符 });
import React , { useContext } from 'react' ; import { AppContext } from './AppContextProvider' ; //假设 Context 定义在单独文件中 function infoDisplay ( ) { const { currentValue, updateValue } = useContext ( AppContext ); return ( < section > < p > 当前 Context: {currentValue} </ p > < button onClick = {() => updateValue('updateContext')}>更改值 </ button > </ section > ); }
将 createContext
与默认值和未定义检查结合使用 在 React + TypeScript 设置中使用 createContext
时,必须注意定义默认值并处理 Context 可能为 undefined
的情况。这将帮助您确保应用保持安全、可预测且不易出现运行时错误。
在 React 中调用 createContext
时,您可以传递默认值作为参数。当读取 Context 的组件不在正确的 Provider 内,或 Provider 本身将值设为 undefined
时, useContext
会返回该值:
interface IThemeContext { theme : 'light' | 'dark' ; switchTheme : () => void ; } const ThemeContext = React . createContext < IThemeContext | null >( null );
当您用 React 的 useContext
Hook 拉取数据但忘记将组件包装在匹配的 Provider 中,或该 Provider 意外发送 undefined
时,Hook 只会返回 undefined
。为了让 TypeScript 满意并为应用提供防止隐蔽运行时错误的安全网,在读取 Context 后始终添加快速检查。这样,当 Context 缺失时,您的组件能冷静应对而非崩溃:
import { createContext, useContext } from 'react' ; interface ContextShape { data : string ; } const customContext = createContext< ContextShape | undefined >( undefined ); export function useCustomContext () { const ctx = useContext ( CustomContext ); if (!ctx) { throw new Error ( 'useCustomContext 必须在 customProvider 内使用' ); } return ctx; } export function CustomProvider ( { children }: { children: React.ReactNode } ) { const contextValue : 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
该文章在 2025/7/21 11:24:24 编辑过