构建可扩展且可维护的 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、 RecordTypeScript 提供了一组内置实用类型,可灵活地从已定义的类型构建新类型。这些工具能帮助塑造组件属性、组织状态、减少冗余并提升 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