咱們公司在德國還有個團隊. 咱們此次要接他們的一個庫. 其中的一個API要求咱們傳入參數, 這個API是這樣定義的:javascript
/* * * @param {Object} input The first object * @param {Object} options The second object * / function init(input, options){ ... } 複製代碼
看到這樣的代碼, 我是崩潰的. 這個input是個Object類型, 很清楚, 但是Object在JavaScript世界裏但是變幻無窮的, 這我到底要傳一個什麼樣的值過去呢.html
這其實就是最典型的例子, 一個"爲何咱們須要使用TypeScript"的例子.java
其實TS還有一些好處, 好比說類型很強大(所以也更難掌握啦), 支持一些現代語言的新特性(如泛型)... 這些咱們在本文中就不贅述了. 我其實更想講解一下在使用TypeScript開發React/ReactNative(下方簡稱R/RN)時的一些坑與注意點, 幫助你更平滑地過渡到TypeScript的世界裏來.react
JS世界裏咱們使用PropTypes
來定義類型, 但它不是很精確, 如PropTypes.object
就不能精確到這個object須要什麼成員, 這樣你一不當心傳少了值, 就會有NPE錯誤.git
TS中對props, state均可以進行限制 - 這適用於類組件與函數組件.github
interface IProps {
name: string;
}
interface IState {
offset: number;
}
class SomeScreen extends React.Component<IProps, IState> {
state = { offset: 0 };
constructor(props: IProps) {
super(props);
console.log(props.name);
}
}
複製代碼
這裏就限定了Props與state的精確類型了. 你不可能再傳錯或少傳props了redux
interface IProps {
name: string;
}
const SomeScreen = (props: IProps) => {
const [offset, setOffset] = useState<number>(0);
console.log(props.name);
};
複製代碼
這時其實就是咱們child view可能有一個, 也可能有多個, 這個可能要根據數據來定的. 好比你給我一個array, 有幾個item我就顯示幾個view.swift
這時這些靈活的子View就能夠被定義爲JSX.Element
類型.react-native
render() {
const children : JSX.Element[] = this.props.data.map((item, index) => {
return <Image source={{ uri: item.url }} style={styles.item} key={`item${index}`}/>;
});
return (
<View style={[this.props.style, styles.container]}>
{children}
</View>
);
}
複製代碼
固然, 這些靈活的子View天然是要有個key了, 否則你會有一個yelloe box來警告你了.數組
這個就要區分了. 類組件與函數組件寫法還不同.
// 函數組件
interface IProps {
id: number;
text?: string;
}
const MyView = (props: IProps) => {
return ( <>.... </> );
};
MyView.defaultProps = {
text: "default"
};
複製代碼
= = = = = = = = = =
// 類組件
class MyScreen extends Component<IProps> {
static defaultProps = {
text: "default"
};
複製代碼
其實這裏有個小坑. 就是你的defaultProps設定其實能夠亂加亂寫屬性, 能夠徹底不按IProps來. 這個TS是無法限定的. 網上有專門解決這些問題的文章, 但在我看來都過於複雜, 反而不如這些寫來得好看. 好在IProps能扛住大多數的檢查, 咱們使用也是使用IProps, 而不直接使用defaultProps.
React中使用ref其實也有多種方式的, 好比說下面兩種:
// React (Approach 1)
const MyView = () => {
let viewRef : HTMLDivElement | null;
return (
<div ref={v => viewRef = v} />
);
};
複製代碼
// React (Approach 2)
const MyView = () => {
const viewRef = createRef<HTMLDivElement | null>();
return (
<div ref={viewRef}/>
);
};
複製代碼
在ref這一塊, React Native異於React的就是類型了, 它再也不是HTML****Element
了.
const MyView = ()=>{
let ref: View|null = null ;
let imageRef = createRef<Image>();
return (
<View ref={ref}>
<Image ref={imageRef} source={require("../a.png")} />
</View>
)
}
複製代碼
固然, 咱們要注意, 涉及到函數組件, 使用ref是要當心些的. 詳細可見React官網說明.
HoC說是高階組件, 但它其實就是個函數.只不過入參與返回值都是組件而已. HoC也是一種組合多種組件的一種方式, 用得好了那重複代碼大量減小, 邏輯分工明確.
固然用得差了, 那就是HoC Hell, 好比說:
(圖片來源: miro.medium.com/max/2586/1*…)
不過在本文中咱們仍是緊貼TS來說解. 使用TS來作HoC, 問題主要仍是在類型上. 你傳進來的組件與返回的新組件, 其類型是什麼.
一個給入參組件添加一個Loading效果的HoC, 能夠這樣寫:
interface IProps {
loading: boolean;
}
const withLoader = <P extends object>(InputComponent: React.ComponentType<P>): React.FC<P & IProps> => {
props.loading ? (... ) : (...)
...
;
複製代碼
注意, 這裏使用的是React.ComponentType
, 這個類型的定義其實就是type ComponentTYpe<P = {}> = ComponentClass<P> | FunctionComponent<P>;
, 即函數組件或類組件都行.
另外, 也注意下Props的聲明. 咱們的入參由於能夠是任意組件, 因此Props不要寫死了, 也就是要用泛型. 至於咱們的HoC要是有什麼本身的需求, 那就能夠用 P & IProps
來組合.
p.s. 這個A & B
, A | B
正是TypeScript的強大之處. 它的類型組件很容易. 這要是換成java, 確定得再定義一個新類型叫C, 而後C中賦值A與B的全部屬性 -- 這就有了重複代碼了.
乍一聽, 這好像不算是什麼麻煩事. 但在TS中, 你要是沒有定義type, 那就是步履維艱. 因此咱們得知道一些常見庫, 還有React中的經常使用屬性究竟是什麼類型. 舉個例子, react-navigation與redux中那幾個dispatch, navigation
都是些什麼類型啊?
下面就是我寫的一個成功的例子:
interface IViewProps {
// ... your own props
}
type IProps = IViewProps &
ViewProps &
NavigationScreenProps &
ReturnType<typeof mapStateToProps> &
ReturnType<typeof mapDispatchToProps>
class MyScreen extends React.Component<IProps, IState> {
// ....
}
複製代碼
其中:
ViewProps
就包含了style
, children
, onLayout
, testID
這些屬性. 注意這是個react-native類NavigationScreenProps
: 它來自於react-navigation
庫, 具備navigation
, screenProps
, navigationOptions
等屬性ReturnType<xxx>
則是對應了redux生成的props. 這一個咱們後面一章節會講到Redux, 這個大名鼎鼎的狀態容器天然不用詳細介紹了. 不過使用TypeScript版本的Redux仍是有些地方要注意的.
Redux中有一個AnyAction
的類型的, 表示任意Action都行. -- 固然也這要遵循基本法, 即flux中的標準action定義
而通常在一個模塊中, 咱們都是說某一個模塊是隻處理特定一些action的. 如audioPlayer模塊就只處理audio play相關的action. 這時咱們能夠這樣:
export interface IAddAction{
type: "Add"
}
export interface IRemoveAction{
type: "Remove",
paylaod: {
id: number
}
}
export type MyAction = IAddAction | IRemoveAction
複製代碼
咱們能夠組合不一樣的action, 變成一個總的Action. 這樣後面的reducer()中就可使用這個總Action. -- 不然的話, 使用範圍更廣的AnyAction
就定位不許, 容易出錯了
這裏的state必定要加個類型. redux由於其是Single Source的緣故, 通常它存儲的state都不小. 特別是咱們有不少個reducer還要一一combine組合以後, 整個應用的全局state就十分大並有層次了. 要是沒有一個明確的類型說明, 半年或一兩年以後, 整個state就很亂, 不知道哪是哪了. 寫過大型項目的同窗確定心有體會了.
export interface IProduct {
id: string;
name: string;
category: IProductCategory;
sku: Sku;
}
export interface MyState {
readonly products: IProduct | null;
}
複製代碼
有了上面的state與action的定義, 如今咱們的reducer就空前地清晰起來了. 在reducer裏面使用state.某field
也會有提示是否正確的, 減小了typo的筆誤可能性.
export const MyReducer : Reducer<MyState, MyAction> = (
state = new MyState(),
action: MyAction
) => {
switch(action.type){
...
}
return state;
}
複製代碼
這裏store就麻煩些了, 不過也更清晰了. 麻煩仍是主要麻煩在整個應用中的各個reducer能夠以不一樣層次地組合起來. -- 這也將影響咱們的state的佈局.
下面就講一個最簡單的例子, 就是隻有一層combineReducer()的.
export interface IAppState {
products: MyState,
books: AnotherState
}
const rootReducer = combineReducer<IAppState>({
products: MyReducer,
books: ANotherReducer
})
export const store = createStore(rootReducer, undefined, applyMiddleware(...));
複製代碼
你要是說你的reducer層次很複雜, 好比說像這樣:
const RootReducer = combineReducer({
oneReducer,
combineReducer(
twoReducer,
combineReducer(fourReducer, fiveReducer)),
})
複製代碼
而後要依樣畫葫蘆地寫state的層次, 是蠻累的. 因此你還能夠這樣來減小你的工做量:
export type IAppState = ReturnType<typeof RootReducer>
複製代碼
我在項目是使用Redux-Saga
來作異步的. 不過你要是想用Thunx
也容易, 就這樣:
export const fetches = async (): Promise<IProduct[]> => {
await wait(1000);
return products;
}
複製代碼
前面講過, 咱們有一個built-in的AnyAction類型, 它的源碼其實就是:
export interface AnyAction extends Action {
// Allows any extra properties to be defined in an action.
[extraProps: string]: any
}
複製代碼
備註: 在TS中, [extraProps: string]: any
中有前半截就是指任意key名字(只要其類型是string就行), 至於value是any類型就行.
這個AnyAction
仍是少用, 這就像any要少用同樣.
若你在項目中使用了Redux-Persist庫, 那上面的IAppState
的定義就有問題了. 由於Redux-Persist會在咱們的appState裏再加一個本身的定義, 因此TS會檢測到類型不匹配而報錯.
舉個栗子來講吧: 咱們如今要存一個state是這樣的: {book: {id: 22, name: "Harry" } }
但一旦使用了Redux-Persist, 那state就變成了: {book: {id: 22, name: "Harray", _persist: {....} } }
因此這時咱們須要這樣改:
interface IAppState {
// book: IBookState // ERROR!!!
book: IBookState & PersistPartial;
}
複製代碼
這個其實在上面講過了, 就是使用ReturnType來作到靈活配置.
type IProps = ReturnType<typeof mapStateToProps>
& ReturnType<typeof mapDispatchToProps>
& ViewProps
複製代碼
我看到不少書或網頁上都是這樣定義中間件的:const middleware = store => next => action => {...}
. 但其實如今的store真的不是指Redux中的那個store了. 其類型是一個新定義的類型: MiddlewareApi
.
看下它的源碼: type MiddlewareAPI = {dispatch: Dispatch, getState: ()=> State}
哈哈, 好吧, 其實和store真的好像.
那咱們要如何用TypeScript來定義一箇中間件呢? -- 其中的麻煩仍是你不知道一些函數入參的類型. 下面這個小片斷就是一個成功的例子:
const myMiddleware = (store: MiddlewareAPI) => (next: Dispatch<AnyAction>)
=> (action: AnyAction) => {
... ...
}
複製代碼
注意: 咱們在Dispatch中都使用了泛型, 否則編譯通不過. 這裏其實也是一個你可能會使用AnyAction的地方. 由於你確實不知道會有什麼樣的action會過來.
先說結論哦, 使用TypeScript來寫測試會比較麻煩. 由於TS會檢測各類類型, 這樣一些Mock的手段會過於hacky而被TS報錯, 說類型不匹配.
下面的例子就是咱們使用jest.mock()
來注入一些mock方法到Worker
類中. 但TS會不知道Workder
還有mockReturnThis()
方法而報錯.
import { work } from "../Worker"
jest.mock("../Worker")
test("some...", ()=>{
work.mockReturnThis(); // ERROR!!!, as TypeScript does not know this method exist
...
})
複製代碼
結果爲了讓其能運行, 你不得不加一個@ts-ignore:
// @ts-ignore
work.mockReturnThis()
複製代碼
但加了@ts-ignore, 老是讓人不舒服的. 因此我我的推薦, 測試仍是用js文件吧.
TypeScript雖然強大, 也不是盡善盡美. 好比Kotlin中很好用的lateinit var
, 在TS中就沒有. TS像KotLin同樣, 一開始聲明const對象就得給值.
不過咱們其實能夠走點偏鋒.
interface People {
id: number,
name: string
}
...
// const p = {} // ERROR! `{}` and `People` are not compatilbe
const p = {} as People
// when time is ripe
p.id = 100
複製代碼
上面的as People
, 指明瞭類型, 還不用全部屬性都賦值, 是方便了. 但也請不要濫用哦, TS的static check正是咱們要用它的地方. 像用了上面的這樣技巧的地方, 咱們最好都是要code review下的.
備註: 要是使用any那就更不可取了. any基本上TS世界的一個大毒瘤, 不是萬不得已不該該使用, 傷人更傷己啊. 之後我可能會專門就這個any, 來說一下如何避免使用any
泛型是個強大的工具, 用過java或swift的同窗都有所瞭解. 對於js的同窗可能比較新, 但也建議去學習一下.
一樣, 在TS中使用泛型要注意. 好比說下面的寫法就報錯了:
你去比照下TS官網上的泛型寫法,一點都不帶差的. 那怎麼還報錯啊?
哈哈,這就是個坑了. 注意, 上面出錯的代碼是在一個.tsx
文件裏的.
.tsx
文件看到<>
時, 首先反應就是, "這是個React的element", 因而想去加載組件.
因此說:
.ts
文件中, 上面的代碼不會報錯..tsx
文件中, 上面的代碼會報錯. 要想修復, 就得告訴TS編譯器, "這是個泛型, 不是組件"具體方法就是:
// ***.tsx
const example = <T extends object>(url: T) : number => {
return 20;
};
複製代碼
好了, TypeScript的一些進階技術就介紹完了. 主要仍是一些不熟悉的三方庫的類型, 和不熟悉的TS的用法 (和java/swift這些語言比起來, 差別性仍是有些的). 之後我如有了更多技巧, 再介紹給你們. 多謝你們捧場~