使用typescript写react app(1)快速入门

2,145 阅读6分钟

本文是《使用typescript写react app》系列的第一篇, 主要介绍ts在react中的基本使用.

组件是React的心脏, 下面让我们使用typescript来"修饰"React组件, 使React组件的属性具有类型.

Functional Components

函数组件在React中是一个纯函数, 一般用于展示层, 下面来看一个简单的卡片组件.

export const Card = ({ title, paragraph }) => (<aside>
  <h2>{ title }</h2>
  <p>
    { paragraph }
  </p>
</aside>)

const el = <Card title="Welcome!" paragraph="To this example" />

首先我们需要为Card组件的参数{ title, paragraph }定义一个类型, 然后使用该类型来"修饰"Card的属性.

type CardProps = {
  title: string,
  paragraph: string
}
export const Card = ({ title, paragraph }: CardProps) => <aside>
  <h2>{ title }</h2>
  ...
</aside>

虽然我们消耗了额外的开发时间定义了Card组件参数的类型, 但是我们得到了以下2个好处:

  1. 当我们在使用Card组件时, 编辑器会自动提示Card组件的参数类型, 以防止我们传错

  2. 即使传错, 在编译阶段也会提示我们传入参数类型错误 这两点好处对于我们开发中是很有帮助的, 编译阶段的错误让我们更早更快的来消除代码中的隐患, 并且在多人协作时, 使用组件时候提示参数类型可以让我们更方便的知道组件如何使用.

如果我们的Card组件中, paragraph 属性是可选的, 我们则可以使用 ? 来定义该类型

type CardProps = {
  title: string,
  paragraph?: string  // the paragraph is optional
}

除了定义React组件的属性之外, 我们还需要定义组件的类型, 幸运的是React官方提供了类型定义, 我们可以拿来直接用, 比如我们想定义Card组件是函数组件, 我们可以这样

import React, { FunctionComponent } from 'react'; /
export const Card: FunctionComponent<CardProps> = ({ title, paragraph }) => <aside>
  <h2>{ title }</h2>
  <p>
    { paragraph }
  </p>
</aside>

这样不仅定义了Card组件的类型是函数组件 FunctionComponent 并且也定义了Card属性的类型, 这样定义类型的方式简洁并且也是 官方推荐 的方式.

其中有一点要注意的是, 如果Card额外接收一个 children 属性的话, 我们无需为 children 定义属性, 该属性的类型会被自动分析出来.

export const Card: FunctionComponent<CardProps> = ({ title, paragraph, children }) => <aside>
  <h2>{ title }</h2>
  <p>
    { paragraph }
  </p>
  { children }
</aside>

Class Components

类组件是老派React App的写法, 现在可以被Hooks完全替代, 我们看看如何使用Typescript修饰Class Components.

import React, { Component } from 'react'; 
type ClockState = {
  time: Date
}

export class Clock extends Component<{}, ClockState> {
  tick() {
    this.setState({
      time: new Date()
    });
  }
  componentWillMount() {
    this.tick();
  }
  componentDidMount() {
    setInterval(() => this.tick(), 1000);
  }
  render() {
    return <p>The current time is {this.state.time.toLocaleTimeString()}</p>
  }
}

第十行代码中 <{}, ClockState> 的第一个空对象是来定义props类型的, 但是Clock没有props所以是空对象, 第二个参数是定义state的类型, Clock组件有一个 time 属性并且类型为javascript的 Date 类型.

Events

React有自己的事件系统, 也为每个事件定义了对应的typings, 因此我们在给事件定义类型的时候可以直接使用react typings.

import React, { Component, MouseEvent } from 'react'; // 引入typings MouseEvent
export class Button extends Component {
  handleClick(event: MouseEvent) { // 定义事件类型
    event.preventDefault();
    alert(event.currentTarget.tagName);
  }
  
  render() {
    return <button onClick={this.handleClick}>
      {this.props.children}
    </button>
  }
}

定义了 event 是 MouseEvent 之后我们还需要为 MouseEvent也定义类型.

export class Button extends Component {
  handleClick(event: MouseEvent<HTMLButtonElement>) { 
    event.preventDefault();
  }
  // 定义MouseEvent是HTMLButtonElement或者HTMLAnchorElement类型
  handleAnotherClick(event: MouseEvent<HTMLButtonElement | HTMLAnchorElement>) {
    event.preventDefault();
  }
  render() {
    return <button onClick={this.handleClick}>
      {this.props.children}
    </button>
  }
}

有一点需要注意的是, InputEvent在TS中不被支持(因为兼容性问题), 因此我们可以简单的为其定义为React合成事件

import React, { Component, SyntheticEvent } from 'react';
export class Input extends Component {
  handleInput(event: SyntheticEvent) {
    event.preventDefault();
    // ...
  }
  render() {
    return <>
      <input type="text" onInput={this.handleInput}/>
    </>
  }
}

Prop Types

React提供了prop-types 来检测props的类型, 但是它是runtime时候才会去检查, 如果配合TS就完美了, 实现了 动静结合 检查props的类型, 让我们的程序更加健壮.

首先安装我们用来检查props的库:

npm install --save prop-types
npm install --save-dev @types/prop-types

我们使用 prop-types 来检查一个组件的props之后还需要再使用TS定义一遍组件的属性吗?显然是不需要的, 我们可以使用 prop-types 中的 InferProps 来帮我们做这些重复的事情.

import PropTypes, { InferProps } from "prop-types";
export function Article({
  title,
  price
}: InferProps<typeof Article.propTypes>) { // 使用InferProps
  return (
    <div className="article">
      <h1>{title}</h1>
      <span>Priced at (incl VAT): {price * 1.2}</span>
    </div>
  );
}
Article.propTypes = {
  title: PropTypes.string.isRequired,
  price: PropTypes.number.isRequired
};

Hooks

Hooks已经逐渐的成为了写React应用的主流方式, 如何将TS将Hooks结合起来呢? 先从 useState 这个使用最频繁之一的Hooks开始

import React, { FunctionComponent, useState } from 'react';
// 定义Counter的类型为FunctionComponent并且为useState定义属性类型
const Counter:FunctionComponent<{ initial?: number }> = ({ initial = 0 }) => {
  const [clicks, setClicks] = useState(initial);
  return <>
    <p>Clicks: {clicks}</p>
    <button onClick={() => setClicks(clicks+1)}>+</button>
    <button onClick={() => setClicks(clicks-1)}>-</button>
  </>
}

useEffect用来处理组件的所有副作用, 比如事件监听和网络请求等. 对于useEffect我们无需定义类型, TS会去检查函数签名从而确定我们传的参数是否正确, 比如我们监听了一个 resize 事件却没有正确的提供给useEffect监听移除函数, TS就会在编译时报错.

useEffect(() => {
  const handler = () => {
    document.title = window.width;
  }
  window.addEventListener('resize', handler);
  // ⚡️ won't compile
  return true;
  // ✅  compiles
  return () => {
    window.removeEventListener('resize', handler);
  }
})

useRef可以帮助我们在函数组件中拿到DOM的引用, 但在 strict mode 下, TS会产生下面这种问题

function TextInputWithFocusButton() {
    // 通常使用null初始化, 在render时候inputEl才会指向DOM
  const inputEl = useRef(null);
  const onButtonClick = () => {
        // TS会报错 因为inputEl初始化时候是null
    inputEl.current.focus();
  };
  return (
    <>
      <input ref={inputEl} type="text" />
      <button onClick={onButtonClick}>Focus the input</button>
    </>
  );
}

因此我们该避免这种类型错误, 通过判断 inputEl 是否为null

function TextInputWithFocusButton() {
  // 定义useRef是一个input元素
  const inputEl = useRef<HTMLInputElement>(null);
  const onButtonClick = () => {
    // 对inputEl进行null检查
    if(inputEl && inputEl.current) {
      inputEl.current.focus();
    } 
  };
  return (
    <>
      <input ref={inputEl} type="text" />
      <button onClick={onButtonClick}>Focus the input</button>
    </>
  );
}

Context API

context API是React跨父子通信的神器, 但是配合TS使用时我们会遇到一些问题

export const AppContext = React.createContext({ 
  authenticated: true,
  lang: 'en',
  theme: 'dark'
});

当创建了一个context后, 我们使用 AppContext.Provider 向下传属性时必须将初始化的属性全部传递下去, 即使有些属性不是我们需要的, 否则TS就会报错

const App = () => {
  // TS会报错 因为少传了其他属性
  return <AppContext.Provider value={ {
    lang: 'de', 
  } }>
    <Header/>
  </AppContext.Provider>
}

我们可以使用TS提供的一个工具 Partial 来解决这个问题.

type ContextProps = { 
  authenticated: boolean,
  lang: string,
  theme: string
};
export const AppContext = 
  React.createContext<Partial<ContextProps>>({}); // 使用Partial
const App = () => {
  // TS不会再报错了
  return <AppContext.Provider value={ {
    authenticated: true,  
  } }>
    <Header/>
  </AppContext.Provider>
}

Conclusion

Typescript已经在逐渐的替代JavaScript, 许多优秀的开源库都迁移到了TS, 使用TS写react应用已经成为了我们必须要掌握的技能, 越早使用越早受益. 本文介绍了TS在react中最基本的使用, 在接下来的文章里面会从优秀的开源库入手, 去探索TS在react应用中的最佳实践, 敬请期待吧!

既然都看到这里了, 点个赞吧 💗