by Christopher Diggins
克里斯托弗·迪金斯(Christopher Diggins)
使用Typescript和React的最佳實踐 (Best practices for using Typescript with React)
There are numerous tools and tutorials to help developers start writing simple React applications with TypeScript. The best practices for using TypeScript in a larger React application are less clear, however.
有許多工具和教程可幫助開發人員開始使用TypeScript編寫簡單的React應用程序。 但是,在較大的React應用程序中使用TypeScript的最佳實踐尚不清楚。
This is especially the case when intergrating with an ecosystem of third party libraries used to address concerns such as: theming, styling, internationalization, logging, asynchronous communication, state-management, and form management.
與第三方庫生態系統集成時尤其如此,該庫用于解決諸如主題,樣式,國際化,日志記錄,異步通信,狀態管理和表單管理等問題。
At Clemex, we develop computational microscopy applications. We recently migrated a React front-end for one of our applications from JavaScript to TypeScript. Overall, we are very pleased with the end result. The consensus is that our codebase is now easier to understand and maintain.
在Clemex ,我們開發計算顯微鏡應用程序。 最近,我們將其中一個應用程序的React前端從JavaScript遷移到TypeScript。 總體而言,我們對最終結果感到非常滿意。 共識是我們的代碼庫現在更易于理解和維護。
That said, our transition was not without some challenges. This article dives into some of the challenges we faced and how we overcame them.
也就是說,我們的過渡并非沒有挑戰。 本文探討了我們面臨的一些挑戰以及如何克服這些挑戰。
The challenges are primarily related to understanding the type signatures of the React API, and particularly those of higher order components. How can we resolve type errors correctly, while retaining the advantages of TypeScript?
這些挑戰主要與理解React API的類型簽名有關,尤其是那些高階組件的類型簽名。 如何在保留TypeScript優點的同時正確解決類型錯誤?
This article attempts to address how to most effectively use TypeScript with React and the ecosystem of supporting libraries. We’ll also address some common areas of confusion.
本文試圖解決如何在React和支持庫的生態系統中最有效地使用TypeScript。 我們還將解決一些常見的混淆領域。
查找庫的類型定義 (Finding type definitions for a library)
A TypeScript program can easily import any JavaScript library. But without type declarations for the imported values and functions, we don’t get the full benefit of using TypeScript.
TypeScript程序可以輕松導入任何JavaScript庫。 但是如果沒有用于導入的值和函數的類型聲明,我們將無法獲得使用TypeScript的全部好處。
Luckily, TypeScript makes it easy to define type annotations for JavaScript libraries, in the form of type declaration files.
幸運的是,TypeScript可以輕松地以類型聲明文件的形式為JavaScript庫定義類型注釋。
Only a few projects today offer TypeScript type definitions directly with the project. However, for many libraries you can usually find an up to date type-definition file in the @types
organization namespace.
如今,只有少數幾個項目直接在項目中提供TypeScript類型定義。 但是,對于許多庫,通常可以在@types
組織名稱空間中找到最新的類型定義文件。
For example, if you look in the TypeScript React Template package.json
, you can see that we use the following type-definition files:
例如,如果您查看TypeScript React Template package.json
,則可以看到我們使用了以下類型定義文件:
"@types/jest": "^22.0.1","@types/node": "^9.3.0","@types/react": "^16.0.34","@types/react-dom": "^16.0.3","@types/redux-logger": "^3.0.5"
The only downside of using external type declarations is that it can be a bit annoying to track down bugs which are due to a versioning mismatch, or subtle bugs in type declaration files themselves. The type declaration files aren’t always supported by the original library authors.
使用外部類型聲明的唯一缺點是,跟蹤由于版本不匹配或類型聲明文件本身中的細微錯誤而導致的bug可能會有些煩人。 原庫作者并不總是支持類型聲明文件。
屬性和狀態字段的編譯時驗證 (Compile-time validation of properties and state fields)
One of the main advantages of using TypeScript in a React application is that the compiler (and the IDE, if configured correctly) can validate all of the necessary properties provided to a component.
在React應用程序中使用TypeScript的主要優點之一是編譯器(和IDE,如果配置正確)可以驗證提供給組件的所有必要屬性。
It can also check that they have the correct type. This replaces the need for a run-time validation as provided by the prop-types
library.
它還可以檢查它們是否具有正確的類型。 這取代了prop-types
庫提供的對運行時驗證的需求。
Here is a simple example of a component with two required properties:
這是具有兩個必需屬性的組件的簡單示例:
import * as React from ‘react’;
export interface CounterDisplayProps { value: number; label: string;}
export class CounterDisplay extends React.PureComponent<CounterDisplayProps> { render(): React.ReactNode { return ( <div> The value of {this.props.label} is {this.props.value} </div> );}
組件作為類或函數 (Components as classes or functions)
With React you can define a new component in two ways: as a function or as a class. The types of these two kinds of components are:
使用React,您可以通過兩種方式定義新組件:作為函數或作為類。 這兩種組件的類型為:
Component Classes ::
React.ComponentClass<
;P>組件類::
React.ComponentClass<
; P>Stateless Functional Components (SFC) ::
React.StatelessComponent<
;P>無狀態功能組件(SFC)::
React.StatelessComponent<
; P>
組件類別 (Component Classes)
A class type is a new concept for developers from a C++/C#/Java background. A class has a special type, which is separate from the type of instance of a class. It is defined in terms of a constructor function. Understanding this is key to understanding type signatures and some of the type errors that may arise.
對于C ++ / C#/ Java背景的開發人員來說,類類型是一個新概念。 類具有特殊的類型,該類型與類的實例類型不同。 它是根據構造函數定義的。 理解這一點是理解類型簽名和可能出現的某些類型錯誤的關鍵。
A ComponentClass
is the type of a constructor function that returns an object which is an instance of a Component
. With some details elided, the essence of the ComponentClass
type definition is:
ComponentClass
是構造函數的類型,該函數返回作為Component
實例的對象。 省略了一些細節, ComponentClass
類型定義的實質是:
interface ComponentClass<P = {}> { new (props: P, context?: any): Component<P, ComponentState>;}
無狀態組件(SFC) (Stateless Components (SFC))
A StatelessComponent
(also known as SFC
) is a function that takes a properties object, optional list of children components, and optional context object. It returns either a ReactElement
or null
.
StatelessComponent
(也稱為SFC
)是一個函數,它接受屬性對象,子組件的可選列表以及可選的上下文對象。 它返回一個ReactElement
或null
。
Despite what the name may suggest, a StatelessComponent
does not have a relationship to a Component
type.
盡管名稱可能暗示,但StatelessComponent
與Component
類型沒有關系。
A simplified version of the definition of the type of a StatelessComponent
and the SFC
alias is:
StatelessComponent
類型和SFC
別名的定義的簡化版本是:
interface StatelessComponent<P = {}> { (props: P & { children?: ReactNode }, context?: any): ReactElement<any> | null;}
type SFC<P = {}> = StatelessComponent<P>;
Prior React 16, SFCs were quite slow. Apparently this has improved with React 16. However, due to the desire for consistent coding style in our code base, we continue to define components as classes.
在React 16之前的版本中,SFC非常慢。 顯然,這已經通過React 16得到了改善 。 但是,由于在我們的代碼庫中需要一致的編碼風格,我們繼續將組件定義為類。
純和非純成分 (Pure and Non-Pure Components)
There are two different types of Component
: pure and non-pure.
有兩種不同類型的Component
:純Component
和非純Component
。
The term ‘pure’ has a very specific meaning in the React framework, unrelated to the term in computer science.
“純”一詞在React框架中具有非常具體的含義,與計算機科學中的術語無關。
A PureComponent
is a component that provides a default implementation ofshouldComponentUpdate
function (which does a shallow compare of this.state
and this.props
).
PureComponent
是一個組件,它提供了shouldComponentUpdate
函數的默認實現(該函數對this.state
和this.props
進行了淺層比較)。
Contrary to a common misconception, a StatelessComponent
is not pure, and a PureComponent
may have a state.
與常見的誤解相反, StatelessComponent
不是純的,而PureComponent
可能具有狀態。
有狀態組件可以(并且應該)從React.PureComponent派生 (Stateful Components can (and should) derive from React.PureComponent)
As stated above, a React component with state can still be considered a Pure component
according to the vernacular of React. In fact, it is a good idea to derive components, which have an internal state, from React.PureComponent.
如上所述,根據React的說法,狀態為React的組件仍可以視為Pure component
。 實際上,從React.PureComponent.
派生具有內部狀態的組件是一個好主意React.PureComponent.
The following is based on Piotr Witek’s popular TypeScript guide, but with the following small modifications:
以下內容基于Piotr Witek流行的TypeScript指南 ,但進行了以下小的修改:
The
setState
function uses a callback to update state based on the previous state as per the React documentation.根據React文檔,
setState
函數使用回調根據先前的狀態更新狀態。We derive from
React.PureComponent
because it does not override the lifecycle functions我們從
React.PureComponent
派生,因為它沒有覆蓋生命周期函數The
State
type is defined as a class so that it can have an initializer.State
類型定義為一個類,以便可以具有初始化程序。- We don’t assign properties to local variables in the render function as it violates the DRY principle, and adds unnecessary lines of code. 我們不會在render函數中為局部變量分配屬性,因為它違反了DRY原理,并增加了不必要的代碼行。
import * as React from ‘react’;export interface StatefulCounterProps { label: string;}
// By making state a class we can define default values.class StatefulCounterState { readonly count: number = 0;};
// A stateful counter can be a React.PureComponentexport class StatefulCounter extends React.PureComponent<StatefulCounterProps, StatefulCounterState>{ // Define readonly state = new State();
// Callbacks should be defined as readonly fields initialized with arrow functions, so you don’t have to bind them // Note that setting the state based on previous state is done using a callback. readonly handleIncrement = () => { this.setState((prevState) => { count: prevState.count + 1 } as StatefulCounterState); }
// We explicitly include the return type render(): React.ReactNode { return ( <div> <span>{this.props.label}: {this.props.count} </span> <button type=”button” onClick={this.handleIncrement}> {`Increment`} </button> </div> ); }}
React無狀態功能組件不是純組件 (React Stateless Functional Components are not Pure Components)
Despite a common misconception, stateless functional components (SFC) are not pure components, which means that they are rendered every time, regardless of whether the properties have changed or not.
盡管有一個普遍的誤解,但無狀態功能組件(SFC)并不是純組件,這意味著它們每次都會被渲染,而不管屬性是否已更改。
鍵入高階組件 (Typing higher-order components)
Many libraries used with React applications provide functions that take a component definition and return a new component definition. These are called Higher-Order Components (or HOCs for short).
React應用程序使用的許多庫都提供了接受組件定義并返回新組件定義的函數。 這些被稱為高階組件 (簡稱HOC )。
A higher-order component might return a StatelessComponent
or a ComponentClass
depending on how it is defined.
高階組件可能會根據其定義方式返回StatelessComponent
或ComponentClass
。
出口違約的困惑 (The confusion of export default)
A common pattern in JavaScript React applications is to define a component, with a particular name (say MyComponent
) and keep it local to a module. Then, export by default the result of wrapping it with one or more HOC.
JavaScript React應用程序中的一種常見模式是定義一個具有特定名稱的組件(例如MyComponent
),并將其保持在模塊本地。 然后,默認情況下導出用一個或多個HOC包裝它的結果。
The anonymous component is imported throughout the application as MyComponent
. This is misleading because the programmer is reusing the same name for two very different things!
匿名組件作為MyComponent
導入整個應用程序。 這具有誤導性,因為程序員將相同的名稱用于兩個截然不同的事物!
To provide proper types, we need to realize that the component returned from a higher-order component is usually not the same type as the component defined in the file.
為了提供適當的類型,我們需要認識到從高階組件返回的組件通常與文件中定義的組件類型不同。
In our team we found it useful to provide names for both the defined component that is kept local to the file (e.g. MyComponentBase
) and to explicitly name a constant with the exported component (e.g. export const MyComponent = injectIntl(MyComponentBase);
).
在我們的團隊中,我們發現為既保留在文件本地的已定義組件(例如MyComponentBase
)提供名稱,并使用導出的組件顯式命名常量(例如export const MyComponent = injectIntl(MyComponentBase);
) export const MyComponent = injectIntl(MyComponentBase);
。
In addition to being more explicit, this avoids the problem of aliasing the definition, which makes understanding and refactoring the code easier.
除了更明確之外,這還避免了別名別名的問題,這使理解和重構代碼更加容易。
注入屬性的HOC (HOCs that Inject Properties)
The majority of HOCs inject properties into your component that do not need to be provided by the consumer of your component. Some examples that we use in our application include:
大多數HOC將不需要用戶使用的屬性注入到您的組件中。 我們在應用程序中使用的一些示例包括:
From material-ui:
withStyles
來自material-ui:
withStyles
From redux-form:
reduxForm
從redux-form:
reduxForm
From react-intl:
injectIntl
從react-intl:
injectIntl
From react-redux:
connect
從react-redux:
connect
內部,外部和注入的屬性 (Inner, Outer, and Injected Properties)
To better understand the relationship between the component returned from the HOC function and the component as it is defined, try this useful mental model:
為了更好地理解從HOC函數返回的組件與定義的組件之間的關系,請嘗試以下有用的思維模型:
Think of the properties expected to be provided by a client of the component as outer properties, and the entirety of the properties visible to the component definition (e.g. the properties used in the render function) as the inner properties. The difference between these two sets of properties are the injected properties.
將期望由組件的客戶端提供的屬性視為外部屬性 ,并將對組件定義可見的整個屬性(例如,渲染函數中使用的屬性)視為內部屬性 。 這兩組屬性之間的差異是注入的屬性 。
類型交集運算符 (The type intersection operator)
In TypeScript, we can combine types in the way we want to for properties using a type-level operator called the intersection operator (&
). The intersection operator will combine the fields from one type with the fields from another type.
在TypeScript中,我們可以使用稱為交集運算符 ( &
)的類型級運算符,以我們想要的屬性組合類型。 相交運算符將合并一種類型的字段和另一種類型的字段。
interface LabelProp { label: string;}
interface ValueProp { value: number;}
// Has both a label field and a value fieldtype LabeledValueProp = LabelProp & ValueProp;
For those of you familiar with set theory, you might be wondering why this isn’t considered a union operator. It is because it is an intersection of the sets of all possible values that satisfy the two type constraints.
對于那些熟悉集合論的人,您可能想知道為什么不將其視為聯合運算符。 這是因為它是滿足兩個類型約束的所有可能值的集合的交集。
定義包裝組件的屬性 (Defining properties for a wrapped component)
When defining a component that will be wrapped with a higher-order component, we have to provide the inner properties to the base type (e.g. React.PureComponent<
;P>).
當定義一個將被高階組件包裝的組件時,我們必須為基本類型提供內部屬性(例如React.PureComponent<
; P>)。
However, we don’t want to define this all in a single exported interface, because these properties do not concern the client of the component: they only want the outer properties.
但是,我們不想在單個導出的接口中定義所有這些內容,因為這些屬性與組件的客戶端無關:它們僅需要外部屬性。
To minimize boilerplate and repetition, we opted to use the intersection operator, at the single point which we need to refer to inner properties type, which is when we pass it as a generic parameter to the base class.
為了最小化樣板和重復,我們選擇在需要引用內部屬性類型的單點使用交集運算符,這是將其作為通用參數傳遞給基類的時間。
interface MyProperties { value: number;}
class MyComponentBase extends React.PureComponent<MyProperties & InjectedIntlProps> { // Now has intl as a property // ...}
export const MyComponent = injectIntl(MyComponentBase); // Has the type React.Component<MyProperties>;
React-Redux連接功能 (The React-Redux connect function)
The connect
function of the React-Redux library is used to retrieve properties required by a component from the Redux store, and to map some of the callbacks to the dispatcher (which triggers actions which trigger updates to the store).
React-Redux庫的connect
函數用于從Redux存儲中檢索組件所需的屬性,并將某些回調映射到分派器(這將觸發觸發更新存儲的操作)。
So, ignoring the optional merge function argument, we have potentially two arguments to connect:
因此,忽略可選的合并功能參數,我們可能會連接兩個參數:
mapStateToProps
mapStateToProps
mapDispatchToProps
mapDispatchToProps
Both of these functions provide their own subset of the inner properties to the component definition.
這兩個函數都為組件定義提供了自己的內部屬性子集。
However, the type signature of connect
is a special case because of the way the type was written. It can infer a type for the properties that are injected and also infer a type for the properties that are remaining.
但是,由于類型的寫入方式, connect
的類型簽名是一種特殊情況。 它可以為注入的屬性推斷類型,也可以為剩余的屬性推斷類型。
This leaves us with two options:
這給我們留下了兩個選擇:
We can split up the interface into the inner properties of
mapStateToProps
and another for themapDispatchToProps
.我們可以分裂的界面進入內性能
mapStateToProps
,另一個用于mapDispatchToProps
。- We can let the type system infer the type for us. 我們可以讓類型系統為我們推斷類型。
In our case, we had to convert roughly 50 connected components from JavaScript to TypeScript.
在我們的案例中,我們不得不將大約50個連接的組件從JavaScript轉換為TypeScript。
They already had formal interfaces generated from the original PropTypes definition (thanks to an open-source tool we used from Lyft).
他們已經有了從原始PropTypes定義生成的正式接口(這要歸功于我們從Lyft使用的開源工具 )。
The value of separating each of these interfaces into outer properties, mapped state properties, and mapped dispatch properties did not seem to outweigh the cost.
將這些接口中的每一個分為外部屬性,映射狀態屬性和映射調度屬性的價值似乎并沒有超過成本。
In the end, using connect
correctly allowed the clients to infer the types correctly. We are satisfied for now, but may revisit the choice.
最后,正確使用connect
允許客戶端正確推斷類型。 我們暫時感到滿意,但可以重新考慮選擇。
幫助React-Redux連接函數推斷類型 (Helping the React-Redux connect function infer types)
The TypeScript type inference engine seems to need at times a delicate touch. The connect
function seems to be one of those cases. Now hopefully this isn’t a case of cargo cult programming, but here are the steps we take to assure the compiler can work out the type.
有時似乎需要TypeScript類型推斷引擎。 connect
功能似乎是其中一種情況。 現在希望這不是對貨物崇拜編程的情況,但是這是我們為確保編譯器可以計算出類型而采取的步驟。
We don’t provide a type to the
mapStateToProps
ormapDispatchToProps
functions, we just let the compiler infer them.我們沒有為
mapStateToProps
或mapDispatchToProps
函數提供類型,我們只是讓編譯器來推斷它們。We define both
mapStateToProps
andmapDispatchToProps
as arrow functions assigned toconst
variables.我們將
mapStateToProps
和mapDispatchToProps
都定義為分配給const
變量的箭頭函數。We use the
connect
as the outermost higher-order component.我們將
connect
用作最外面的高階組件。We don’t combine multiple higher-order components using a
compose
function.我們不使用
compose
函數組合多個高階分量。
The properties that are connected to the store in mapStateToProps
and mapDispatchToProps
must not be declared as optional, otherwise you can get type errors in the inferred type.
不得將在mapStateToProps
和mapDispatchToProps
中連接到商店的屬性聲明為可選,否則您會在推斷的類型中得到類型錯誤。
最后的話 (Final Words)
In the end, we found that using TypeScript made our applications easier to understand. It helped deepen our understanding of React and the architecture of our own application.
最后,我們發現使用TypeScript使我們的應用程序更易于理解。 它有助于加深我們對React和我們自己的應用程序架構的理解。
Using TypeScript correctly within the context of additional libraries designed to extend React required additional effort, but is definitely worth it.
在旨在擴展React的其他庫的上下文中正確使用TypeScript需要付出額外的努力,但這絕對是值得的。
If you are just starting off with TypeScript in React, the following guides will be useful:
如果您只是從React中的TypeScript開始,那么以下指南將非常有用:
Microsoft TypeScript React Starter
Microsoft TypeScript React Starter
Microsoft’s TypeScript React Conversion Guide
Microsoft的TypeScript React轉換指南
Lyft’s React JavaScript to TypeScript Converter
Lyft的React JavaScript到TypeScript轉換器
After that I recommend reading through the following articles:
之后,我建議您通讀以下文章:
React Higher-Order Component Patterns in TypeScript by James Ravenscroft
在TypeScript中React高階組件模式 James Ravenscroft
Piotr Witek’s React-Redux TypeScript Guide
Piotr Witek的React-Redux TypeScript指南
致謝 (Acknowledgements)
Many thanks to the members of Clemex team for their collaboration on this article, working together to figure out how to use TypeScript to its best potential in React applications, and developing the open-source TypeScript React Template project on GitHub.
非常感謝Clemex團隊的成員在本文上的協作,共同努力找出如何在React應用程序中發揮TypeScript的最大潛力,并在GitHub上開發開源TypeScript React Template項目 。
翻譯自: https://www.freecodecamp.org/news/effective-use-of-typescript-with-react-3a1389b6072a/