用TypeScript編寫React的最佳實(shí)踐
本文譯自 https://www.sitepoint.com/react-with-typescript-best-practices/

如今, React 和 TypeScript 是許多開(kāi)發(fā)人員正在使用的兩種很棒的技術(shù)。但是把他們結(jié)合起來(lái)使用就變得很棘手了,有時(shí)很難找到正確的答案。不要擔(dān)心,本文我們來(lái)總結(jié)一下兩者結(jié)合使用的最佳實(shí)踐。
React 和 TypeScript 如何一起使用
在開(kāi)始之前,讓我們回顧一下 React 和 TypeScript 是如何一起工作的。React 是一個(gè) “用于構(gòu)建用戶界面的 JavaScript 庫(kù)” ,而 TypeScript 是一個(gè) “可編譯為普通 JavaScript 的 JavaScript類型化超集” 。通過(guò)同時(shí)使用它們,我們實(shí)際上是使用 JavaScript 的類型化版本來(lái)構(gòu)建 UI。
將它們一起使用的原因是為了獲得靜態(tài)類型化語(yǔ)言( TypeScript )對(duì) UI 的好處:減少 JS 帶來(lái)的 bug,讓前端開(kāi)發(fā)更安全。
TypeScript 會(huì)編譯我的 React 代碼嗎?
一個(gè)經(jīng)常被提到的常見(jiàn)問(wèn)題是 TypeScript 是否編譯你的 React 代碼。TypeScript 的工作原理類似于下面的方式:
- TS:“嘿,這是你所有的UI代碼嗎?”
- React:“是的!”
- TS:“酷!我將對(duì)其進(jìn)行編譯,并確保你沒(méi)有錯(cuò)過(guò)任何內(nèi)容?!?/li>
- React:“聽(tīng)起來(lái)對(duì)我很好!”
因此,答案是肯定的!但是稍后,當(dāng)我們介紹 tsconfig.json 配置時(shí),大多數(shù)時(shí)候你都想使用 "noEmit": true 。這是因?yàn)橥ǔG闆r下,我們只是利用 TypeScript 進(jìn)行類型檢查。
概括地說(shuō), TypeScript 編譯你的 React 代碼以對(duì)你的代碼進(jìn)行類型檢查。在大多數(shù)情況下,它不會(huì)發(fā)出任何 JavaScript 輸出。輸出仍然類似于非 TypeScript React 項(xiàng)目。
TypeScript 可以與 React 和 Webpack 一起使用嗎?
是的, TypeScript 可以與 React 和 webpack 一起使用。幸運(yùn)的是,官方 TypeScript 手冊(cè)對(duì)此提供了配置指南。
希望這能使你輕而易舉地了解兩者的工作方式?,F(xiàn)在,進(jìn)入最佳實(shí)踐!
最佳實(shí)踐
我們研究了最常見(jiàn)的問(wèn)題,并整理了 React with TypeScript 最常用的一些寫法和配置。這樣,通過(guò)使用本文作為參考,你可以在項(xiàng)目中遵循最佳實(shí)踐。
配置
配置是開(kāi)發(fā)中最無(wú)趣但是最重要的部分之一。我們?cè)鯓硬拍茉谧疃痰臅r(shí)間內(nèi)完成這些配置,從而提供最大的效率和生產(chǎn)力?我們一起來(lái)討論下面的配置
tsconfig.jsonESLint/PrettierVS Code擴(kuò)展和配置
項(xiàng)目初始化
初始化一個(gè) React/TypeScript 應(yīng)用程序的最快方法是 create-react-app 與 TypeScript 模板一起使用。你可以運(yùn)行以下面的命令:
npx create-react-app my-app --template typescript
這可以讓你開(kāi)始使用 TypeScript 編寫 React 。一些明顯的區(qū)別是:
.tsx:TypeScript JSX文件擴(kuò)展tsconfig.json:具有一些默認(rèn)配置的TypeScript配置文件react-app-env.d.ts:TypeScript聲明文件,可以進(jìn)行允許引用SVG這樣的配置
tsconfig.json
幸運(yùn)的是,最新的 React/TypeScript 會(huì)自動(dòng)生成 tsconfig.json ,并且默認(rèn)帶有一些最基本的配置。我們建議你修改成下面的內(nèi)容:
{
??"compilerOptions":?{
????"target":?"es5",?//?指定?ECMAScript?版本
????"lib":?[
??????"dom",
??????"dom.iterable",
??????"esnext"
????],?//?要包含在編譯中的依賴庫(kù)文件列表
????"allowJs":?true,?//?允許編譯?JavaScript?文件
????"skipLibCheck":?true,?//?跳過(guò)所有聲明文件的類型檢查
????"esModuleInterop":?true,?//?禁用命名空間引用?(import?*?as?fs?from?"fs")?啟用?CJS/AMD/UMD?風(fēng)格引用?(import?fs?from?"fs")
????"allowSyntheticDefaultImports":?true,?//?允許從沒(méi)有默認(rèn)導(dǎo)出的模塊進(jìn)行默認(rèn)導(dǎo)入
????"strict":?true,?//?啟用所有嚴(yán)格類型檢查選項(xiàng)
????"forceConsistentCasingInFileNames":?true,?//?不允許對(duì)同一個(gè)文件使用不一致格式的引用
????"module":?"esnext",?//?指定模塊代碼生成
????"moduleResolution":?"node",?//?使用?Node.js?風(fēng)格解析模塊
????"resolveJsonModule":?true,?//?允許使用?.json?擴(kuò)展名導(dǎo)入的模塊
????"noEmit":?true,?//?不輸出(意思是不編譯代碼,只執(zhí)行類型檢查)
????"jsx":?"react",?//?在.tsx文件中支持JSX
????"sourceMap":?true,?//?生成相應(yīng)的.map文件
????"declaration":?true,?//?生成相應(yīng)的.d.ts文件
????"noUnusedLocals":?true,?//?報(bào)告未使用的本地變量的錯(cuò)誤
????"noUnusedParameters":?true,?//?報(bào)告未使用參數(shù)的錯(cuò)誤
????"experimentalDecorators":?true,?//?啟用對(duì)ES裝飾器的實(shí)驗(yàn)性支持
????"incremental":?true,?//?通過(guò)從以前的編譯中讀取/寫入信息到磁盤上的文件來(lái)啟用增量編譯
????"noFallthroughCasesInSwitch":?true?
??},
??"include":?[
????"src/**/*"?//?***?TypeScript文件應(yīng)該進(jìn)行類型檢查?***
??],
??"exclude":?["node_modules",?"build"]?//?***?不進(jìn)行類型檢查的文件?***
}
其他建議來(lái)自 react-typescript-cheatsheet 社區(qū)
ESLint / Prettier
為了確保你的代碼遵循項(xiàng)目或團(tuán)隊(duì)的規(guī)則,并且樣式保持一致,建議你設(shè)置 ESLint 和 Prettier 。為了讓它們配合的很好,請(qǐng)按照以下步驟進(jìn)行設(shè)置。
1.安裝依賴
yarn add eslint @typescript-eslint/parser @typescript-eslint/eslint-plugin eslint-plugin-react --dev
2.在根目錄下創(chuàng)建一個(gè)eslintrc.js 文件并添加以下內(nèi)容:
module.exports?=??{
??parser:??'@typescript-eslint/parser',??//?指定ESLint解析器
??extends:??[
????'plugin:react/recommended',??//?使用來(lái)自?@eslint-plugin-react?的推薦規(guī)則
????'plugin:@typescript-eslint/recommended',??//?使用來(lái)自@typescript-eslint/eslint-plugin的推薦規(guī)則
??],
??parserOptions:??{
??ecmaVersion:??2018,??//?允許解析最新的?ECMAScript?特性
??sourceType:??'module',??//?允許使用?import
??ecmaFeatures:??{
????jsx:??true,??//?允許對(duì)JSX進(jìn)行解析
??},
??},
??rules:??{
????//?自定義規(guī)則
????//?e.g.?"@typescript-eslint/explicit-function-return-type":?"off",
??},
??settings:??{
????react:??{
??????version:??'detect',??//?告訴?eslint-plugin-react?自動(dòng)檢測(cè)?React?的版本
????},
??},
};
3.添加 Prettier 依賴
yarn add prettier eslint-config-prettier eslint-plugin-prettier --dev
4.在根目錄下創(chuàng)建一個(gè) .prettierrc.js 文件并添加以下內(nèi)容:
module.exports?=??{
??semi:??true,
??trailingComma:??'all',
??singleQuote:??true,
??printWidth:??120,
??tabWidth:??4,
};
- 更新
.eslintrc.js文件:
module.exports?=??{
??parser:??'@typescript-eslint/parser',??//?指定ESLint解析器
??extends:??[
????'plugin:react/recommended',??//?使用來(lái)自?@eslint-plugin-react?的推薦規(guī)則
????'plugin:@typescript-eslint/recommended',??//?使用來(lái)自@typescript-eslint/eslint-plugin的推薦規(guī)則
????'prettier/@typescript-eslint',??//?使用?ESLint?-config-prettier?禁用來(lái)自@typescript-eslint/?ESLint?與?prettier?沖突的?ESLint?規(guī)則
????'plugin:prettier/recommended',??
??],
??parserOptions:??{
??ecmaVersion:??2018,??//?允許解析最新的?ECMAScript?特性
??sourceType:??'module',??//?允許使用?import
??ecmaFeatures:??{
????jsx:??true,??//?允許對(duì)JSX進(jìn)行解析
??},
??},
??rules:??{
????//?自定義規(guī)則
????//?e.g.?"@typescript-eslint/explicit-function-return-type":?"off",
??},
??settings:??{
????react:??{
??????version:??'detect',??//?告訴?eslint-plugin-react?自動(dòng)檢測(cè)?React?的版本
????},
??},
};
VSCode 擴(kuò)展和設(shè)置
我們添加了 ESLint 和 Prettier ,下一步就是在保存時(shí)自動(dòng)修復(fù)/美化我們的代碼。
首先,安裝 VSCode 的 ESLint extension 和 Prettier extension 。這將使 ESLint 與您的編輯器無(wú)縫集成。
接下來(lái),通過(guò)將以下內(nèi)容添加到您的中來(lái)更新工作區(qū)設(shè)置 .vscode/settings.json :
{
????"editor.formatOnSave":?true
}
保存時(shí), VS Code 會(huì)發(fā)揮它的魔力并修復(fù)您的代碼。很棒!
組件
React 的核心概念之一是組件。在這里,我們將引用 React v16.8 以后的標(biāo)準(zhǔn)組件,這意味著使用 Hook 而不是類的組件。
通常,一個(gè)基本的組件有很多需要關(guān)注的地方。讓我們看一個(gè)例子:
import?React?from?'react'
//?函數(shù)聲明式寫法
function?Heading():?React.ReactNode?{
??return?<h1>My?Website?Headingh1>
}
//?函數(shù)擴(kuò)展式寫法
const?OtherHeading:?React.FC?=?()?=>?<h1>My?Website?Headingh1>
注意這里的關(guān)鍵區(qū)別。在第一個(gè)例子中,我們使用函數(shù)聲明式寫法,我們注明了這個(gè)函數(shù)返回值是 React.ReactNode 類型。相反,第二個(gè)例子使用了一個(gè)函數(shù)表達(dá)式。因?yàn)榈诙€(gè)實(shí)例返回一個(gè)函數(shù),而不是一個(gè)值或表達(dá)式,所以我們我們注明了這個(gè)函數(shù)返回值是 React.FC 類型。
記住這兩種方式可能會(huì)讓人混淆。這主要取決于設(shè)計(jì)選擇。無(wú)論您選擇在項(xiàng)目中使用哪個(gè),都要始終如一地使用它。
Props
我們將介紹的下一個(gè)核心概念是 Props。你可以使用 interface 或 type 來(lái)定義 Props 。讓我們看另一個(gè)例子:
import?React?from?'react'
interface?Props?{
??name:?string;
??color:?string;
}
type?OtherProps?=?{
??name:?string;
??color:?string;
}
//?Notice?here?we're?using?the?function?declaration?with?the?interface?Props
function?Heading({?name,?color?}:?Props):?React.ReactNode?{
??return?<h1>My?Website?Headingh1>
}
//?Notice?here?we're?using?the?function?expression?with?the?type?OtherProps
const?OtherHeading:?React.FC?=?({?name,?color?})?=>
??<h1>My?Website?Headingh1>
關(guān)于 interface 或 type ,我們建議遵循 react-typescript-cheatsheet 社區(qū)提出的準(zhǔn)則:
- 在編寫庫(kù)或第三方環(huán)境類型定義時(shí),始終將
interface用于公共API的定義。 - 考慮為你的 React 組件的
State和Props使用type,因?yàn)樗芗s束?!?/li>
讓我們?cè)倏匆粋€(gè)示例:
import?React?from?'react'
type?Props?=?{
???/**?color?to?use?for?the?background?*/
??color?:?string;
???/**?standard?children?prop:?accepts?any?valid?React?Node?*/
??children:?React.ReactNode;
???/**?callback?function?passed?to?the?onClick?handler*/
??onClick:?()??=>?void;
}
const?Button:?React.FC?=?({?children,?color?=?'tomato',?onClick?})?=>?{
???return?<button?style={{?backgroundColor:?color?}}?onClick={onClick}>{children}button>
}
在此 組件中,我們?yōu)?Props 使用 type。每個(gè) Props 上方都有簡(jiǎn)短的說(shuō)明,以為其他開(kāi)發(fā)人員提供更多背景信息。? 表示 Props 是可選的。children props 是一個(gè) React.ReactNode 表示它還是一個(gè) React 組件。
通常,在 React 和 TypeScript 項(xiàng)目中編寫 Props 時(shí),請(qǐng)記住以下幾點(diǎn):
- 始終使用
TSDoc標(biāo)記為你的Props添加描述性注釋/** comment */。 - 無(wú)論你為組件
Props使用type還是interfaces,都應(yīng)始終使用它們。 - 如果
props是可選的,請(qǐng)適當(dāng)處理或使用默認(rèn)值。
Hooks
幸運(yùn)的是,當(dāng)使用 Hook 時(shí), TypeScript 類型推斷工作得很好。這意味著你沒(méi)有什么好擔(dān)心的。舉個(gè)例子:
//?`value`?is?inferred?as?a?string
//?`setValue`?is?inferred?as?(newValue:?string)?=>?void
const?[value,?setValue]?=?useState('')
TypeScript 推斷出 useState 鉤子給出的值。這是一個(gè) React 和 TypeScript 協(xié)同工作的成果。
在極少數(shù)情況下,你需要使用一個(gè)空值初始化 Hook ,可以使用泛型并傳遞聯(lián)合以正確鍵入 Hook 。查看此實(shí)例:
type?User?=?{
??email:?string;
??id:?string;
}
//?the?generic?is?the?>
//?the?union?is?the?User?|?null
//?together,?TypeScript?knows,?"Ah,?user?can?be?User?or?null".
const?[user,?setUser]?=?useStatenull>(null);
下面是一個(gè)使用 userReducer 的例子:
type?AppState?=?{};
type?Action?=
??|?{?type:?"SET_ONE";?payload:?string?}
??|?{?type:?"SET_TWO";?payload:?number?};
export?function?reducer(state:?AppState,?action:?Action):?AppState?{
??switch?(action.type)?{
????case?"SET_ONE":
??????return?{
????????...state,
????????one:?action.payload?//?`payload`?is?string
??????};
????case?"SET_TWO":
??????return?{
????????...state,
????????two:?action.payload?//?`payload`?is?number
??????};
????default:
??????return?state;
??}
}
可見(jiàn),Hooks 并沒(méi)有為 React 和 TypeScript 項(xiàng)目增加太多復(fù)雜性。
常見(jiàn)用例
本節(jié)將介紹人們?cè)趯?TypeScript 與 React 結(jié)合使用時(shí)一些常見(jiàn)的坑。我們希望通過(guò)分享這些知識(shí),您可以避免踩坑,甚至可以與他人分享這些知識(shí)。
處理表單事件
最常見(jiàn)的情況之一是 onChange 在表單的輸入字段上正確鍵入使用的。這是一個(gè)例子:
import?React?from?'react'
const?MyInput?=?()?=>?{
??const?[value,?setValue]?=?React.useState('')
??//?事件類型是“ChangeEvent”
??//?我們將?“HTMLInputElement”?傳遞給?input
??function?onChange(e:?React.ChangeEvent )?{
????setValue(e.target.value)
??}
??return?<input?value={value}?onChange={onChange}?id="input-example"/>
}
擴(kuò)展組件的 Props
有時(shí),您希望獲取為一個(gè)組件聲明的 Props,并對(duì)它們進(jìn)行擴(kuò)展,以便在另一個(gè)組件上使用它們。但是你可能想要修改一兩個(gè)屬性。還記得我們?nèi)绾慰创齼煞N類型組件 Props、type 或 interfaces 的方法嗎?取決于你使用的組件決定了你如何擴(kuò)展組件 Props 。讓我們先看看如何使用 type:
import?React?from?'react';
type?ButtonProps?=?{
????/**?the?background?color?of?the?button?*/
????color:?string;
????/**?the?text?to?show?inside?the?button?*/
????text:?string;
}
type?ContainerProps?=?ButtonProps?&?{
????/**?the?height?of?the?container?(value?used?with?'px')?*/
????height:?number;
}
const?Container:?React.FC?=?({?color,?height,?width,?text?})?=>?{
??return?<div?style={{?backgroundColor:?color,?height:?`${height}px`?}}>{text}div>
}
如果你使用 interface 來(lái)聲明 props,那么我們可以使用關(guān)鍵字 extends 從本質(zhì)上“擴(kuò)展”該接口,但要進(jìn)行一些修改:
import?React?from?'react';
interface?ButtonProps?{
????/**?the?background?color?of?the?button?*/
????color:?string;
????/**?the?text?to?show?inside?the?button?*/
????text:?string;
}
interface?ContainerProps?extends?ButtonProps?{
????/**?the?height?of?the?container?(value?used?with?'px')?*/
????height:?number;
}
const?Container:?React.FC?=?({?color,?height,?width,?text?})?=>?{
??return?<div?style={{?backgroundColor:?color,?height:?`${height}px`?}}>{text}div>
}
兩種方法都可以解決問(wèn)題。由您決定使用哪個(gè)。就個(gè)人而言,擴(kuò)展 interface 更具可讀性,但最終取決于你和你的團(tuán)隊(duì)。
第三方庫(kù)
無(wú)論是用于諸如 Apollo 之類的 GraphQL 客戶端還是用于諸如 React Testing Library 之類的測(cè)試,我們經(jīng)常會(huì)在 React 和 TypeScript 項(xiàng)目中使用第三方庫(kù)。發(fā)生這種情況時(shí),你要做的第一件事就是查看這個(gè)庫(kù)是否有一個(gè)帶有 TypeScript 類型定義 @types 包。你可以通過(guò)運(yùn)行:
#yarn
yarn add @types/
#npm
npm install @types/
例如,如果您使用的是 Jest ,則可以通過(guò)運(yùn)行以下命令來(lái)實(shí)現(xiàn):
#yarn
yarn add @types/jest
#npm
npm install @types/jest
這樣,每當(dāng)在項(xiàng)目中使用 Jest 時(shí),就可以增加類型安全性。
該 @types 命名空間被保留用于包類型定義。它們位于一個(gè)名為 DefinitelyTyped 的存儲(chǔ)庫(kù)中,該存儲(chǔ)庫(kù)由 TypeScript 團(tuán)隊(duì)和社區(qū)共同維護(hù)。
總結(jié)
由于信息量大,以最佳方式一起使用 React 和 TypeScript 需要一些學(xué)習(xí)時(shí)間,但是從長(zhǎng)遠(yuǎn)來(lái)看,其收益是巨大的。在本文中,我們介紹了配置,組件,Props,Hook,常見(jiàn)用例和第三方庫(kù)。盡管我們可以更深入地研究各個(gè)領(lǐng)域,但這應(yīng)涵蓋幫助您遵循最佳實(shí)踐所需的 80% 。
如果您希望看到它的實(shí)際效果,可以在GitHub上看到這個(gè)示例。
https://github.com/jsjoeio/react-ts-example
推薦閱讀
我的公眾號(hào)能帶來(lái)什么價(jià)值?(文末有送書規(guī)則,一定要看)
每個(gè)前端工程師都應(yīng)該了解的圖片知識(shí)(長(zhǎng)文建議收藏)
為什么現(xiàn)在面試總是面試造火箭?
