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这些语言比起来, 差别性仍是有些的). 之后我如有了更多技巧, 再介绍给你们. 多谢你们捧场~

相关文章
相关标签/搜索