TypeScript for React (Native) 進階

I. 爲什麼要用TypeScript

咱們公司在德國還有個團隊. 咱們此次要接他們的一個庫. 其中的一個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

  • 1). TypeScript(下方簡稱TS)幫助咱們檢測類型, 方便使用/閱讀其它的模塊
  • 2). TypeScript是強制檢查的, 是不腐爛的. 而JavaScript(下方簡稱JS)中你就是加一個註釋說"option是{isA: boolean, id: nubmer}", 這也不太好. 由於你之後改了option的結構, 大概念你的註釋是沒有變的. 但TS不會. TS一改option的結構, 其調用處就會報錯, 說已經對應不上了. 強制你修改過來

其實TS還有一些好處, 好比說類型很強大(所以也更難掌握啦), 支持一些現代語言的新特性(如泛型)... 這些咱們在本文中就不贅述了. 我其實更想講解一下在使用TypeScript開發React/ReactNative(下方簡稱R/RN)時的一些坑與注意點, 幫助你更平滑地過渡到TypeScript的世界裏來.react

II. React

JS世界裏咱們使用PropTypes來定義類型, 但它不是很精確, 如PropTypes.object就不能精確到這個object須要什麼成員, 這樣你一不當心傳少了值, 就會有NPE錯誤.git

TS中對props, state均可以進行限制 - 這適用於類組件與函數組件.github

1. class組件

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

2. function組件

interface IProps {
  name: string;
}

const SomeScreen = (props: IProps) => {
  const [offset, setOffset] = useState<number>(0);
  console.log(props.name);
};
複製代碼

3. 進階: child view是flexible的場景

這時其實就是咱們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來警告你了.數組

4. 默認屬性值

這個就要區分了. 類組件與函數組件寫法還不同.

// 函數組件
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.

5. 引用(ref)

5.1 React

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}/>
  );
};
複製代碼

5.2 React Native

在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官網說明.

6. 高階組件(HoC)

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的全部屬性 -- 這就有了重複代碼了.

7. 平常開發中經常使用的屬性

乍一聽, 這好像不算是什麼麻煩事. 但在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. 這一個咱們後面一章節會講到

III. Redux

Redux, 這個大名鼎鼎的狀態容器天然不用詳細介紹了. 不過使用TypeScript版本的Redux仍是有些地方要注意的.

1. action

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就定位不許, 容易出錯了

2. state

這裏的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;
}
複製代碼

3. reducer

有了上面的state與action的定義, 如今咱們的reducer就空前地清晰起來了. 在reducer裏面使用state.某field也會有提示是否正確的, 減小了typo的筆誤可能性.

export const MyReducer : Reducer<MyState, MyAction> = (
  state = new MyState(),
  action: MyAction
) => {
  switch(action.type){
    ...
  }
  return state;
}
複製代碼

4. store

這裏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>
複製代碼

5. async action

我在項目是使用Redux-Saga來作異步的. 不過你要是想用Thunx也容易, 就這樣:

export const fetches = async (): Promise<IProduct[]> => {
  await wait(1000);
  return products;
}
複製代碼

6. AnyAction

前面講過, 咱們有一個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要少用同樣.

7. Redux-Persist

若你在項目中使用了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;
  
}
複製代碼

8. React-Redux

這個其實在上面講過了, 就是使用ReturnType來作到靈活配置.

type IProps = ReturnType<typeof mapStateToProps> 
		& ReturnType<typeof mapDispatchToProps>
		& ViewProps
複製代碼

9. Middlewares

我看到不少書或網頁上都是這樣定義中間件的: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會過來.

IV. 測試

先說結論哦, 使用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文件吧.

V. 其它

1. lazy init

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

2. 泛型

泛型是個強大的工具, 用過java或swift的同窗都有所瞭解. 對於js的同窗可能比較新, 但也建議去學習一下.

一樣, 在TS中使用泛型要注意. 好比說下面的寫法就報錯了:

你去比照下TS官網上的泛型寫法,一點都不帶差的. 那怎麼還報錯啊?

哈哈,這就是個坑了. 注意, 上面出錯的代碼是在一個.tsx文件裏的.

.tsx文件看到<>時, 首先反應就是, "這是個React的element", 因而想去加載組件.

因此說:

  • .ts文件中, 上面的代碼不會報錯.
  • .tsx文件中, 上面的代碼會報錯. 要想修復, 就得告訴TS編譯器, "這是個泛型, 不是組件"

具體方法就是:

// ***.tsx
const example = <T extends object>(url: T) : number => {
  return 20;
};
複製代碼

VI. 總結

好了, TypeScript的一些進階技術就介紹完了. 主要仍是一些不熟悉的三方庫的類型, 和不熟悉的TS的用法 (和java/swift這些語言比起來, 差別性仍是有些的). 之後我如有了更多技巧, 再介紹給你們. 多謝你們捧場~

相關文章
相關標籤/搜索