一开始在 React 里用 TS 我是拒绝的。能不能不要再爆红了 QAQ

但是当我用了半年 TS、前几天用 JS 起了个小项目的时候,才感觉没类型提示真的有点不习惯了。JS 不会给你提示,要一通 console.log 大法慢慢调试。而 TS 则会预先对项目静态编译,这样可以避免拼写错误问题,写好的接口类型也方便多人协作时快速了解变量结构。

而困扰我在 React 里写 TS 的最大问题,其实就是类型该如何写,包括 React 中出现的特定结构的类型、事件类型等等,以及这些类型应该怎么才算写得比较规范。

约等于翻译了一下参考文档,仅作个人写项目参考

React 中常见的类型

函数式组件

React.FC<Props>|React.FunctionComponent<Props>

import { FC } from 'React';
const Component: FC<Props> => {}

class 式组件

React.Component<Props, State>

import { Component } from 'React';
class component extends Component<Props, State> => {}

高阶组件(HOC)

React.ComponentProps<typeof XXX>

import { Diff } from 'utility-types'
interface InjectedProps {
  // ...
}
const withState = <WrappedProps extends InjectedProps>(
  WrappedComponent: React.ComponentType<WrappedProps>
) => {
  type HOCProps = Diff<
    React.ComponentProps<typeof WrappedComponent>,
    InjectedProps
  > & {
    // 填写这个HOC扩充的props
  };
  }
  type HOCState = {
    readonly state: string;
  }
  handleChangeState = () => {
    this.setState({ state: 'new state' })
  }
  class HOC extends React.Component<HOCProps, State> {
    render(){
      const {...restProps} = this.props;
      return
        <WrappedComponent
          {...(restProps as WrappedProps)}
          state={state}
          onChangeState={handleChangeState}
        />
    }
  }
  return HOC
}

React 元素

React.ReactElement|JSX.Element

const element: React.ReactElement = <a /> || <Component />;

React 节点

React.ReactNode

除了元素,还包括字符串、数字、布尔值、null、undefined 等

const node: React.ReactNode = <a /> || <Component /> || "string";

style 类型

React.CSSProperties

const style: React.CSSProperties = {
  color: "red",
  fontSize: "12px",
};
const element = <div style={style} />;

HTML 属性类型

React.XXXHTMLAttributes<HTMLXXXElement>

用于拓展 HTML 元素的属性

// 例如定制一个Button组件,该type可以保留button的原有属性
const Button:React.FC<Props & React.ButtonHTMLAttributes<HTMLButtonElement> = props=>{...}
<Button type="button" disabled={true} />

声明事件处理程序

React.ReactEventHandler<HTMLXXXElement>

const handleChange: React.ReactEventHandler<HTMLInputElement> = (e) => {
  console.log(e.target.value);
};

<input onChange={handleChange} />;

具体事件类型

React.XXXEvent<HTMLXXXElement>

一些常见的事件示例:ChangeEvent, FormEvent, FocusEvent, KeyboardEvent, MouseEvent, DragEvent, PointerEvent, WheelEvent, TouchEvent

const handleChange: React.ChangeEvent<HTMLInputElement> = (e) => {
  console.log(e.target.value);
};
<input onChange={handleChange} />;

常见的编写需求

定义默认 Props

Props 的 type 可以使用 Partial,保证在使用组件时可以只传入部分 Props。

建议返回类型选择 JSX.Element 而非 React.FC,因为 React.FC 在 props 有默认值时可能会不正常工作

type Props{
  name: string;
  age: number;
}
export const Component: React.FC<Props> = (props):JSX.Element => {
  const { name, age } = props;
  return <div>{name} {age}</div>
}
Component.defaultProps = {
  name: 'name',
  age: 0,
}

另外,在Typescript 配合 React 实践 发现一个有意思的利用高阶组件设置默认 props 的写法,供参考

export const withDefaultProps = <
  P extends object,
  DP extends Partial<P> = Partial<P>
>(
  defaultProps: DP,
  Cmp: ComponentType<P>,
) => {
  // 提取出必须的属性
  type RequiredProps = Omit<P, keyof DP>;
  // 重新创建我们的属性定义,通过一个相交类型,将所有的原始属性标记成可选的,必选的属性标记成可选的
  type Props = Partial<DP> & Required<RequiredProps>;

  Cmp.defaultProps = defaultProps;

  // 返回重新的定义的属性类型组件,通过将原始组件的类型检查关闭,然后再设置正确的属性类型
  return (Cmp as ComponentType<any>) as ComponentType<Props>;
};

组件中将传入子组件

定义 props 的 type 时可以使用React.PropsWithChildren<{type}>,如此组件默认会接收传入的子组件,而不用另行定义

type Type = {
  name: string;
  age: number;
};
type Props = React.PropsWithChildren<Type>;
export const Component: React.FC<Props> = (props) => {
  const { children, ...rest } = props;
  return <div {...rest}>{children}</div>;
};

使用 Context

  1. Context

定义并创建默认 props + 使用React.createContext创建 Context

// 定义默认props类型
type Themes = {
  dark: React.CSSProperties;
  light: React.CSSProperties;
};
// 创建默认props
export const themes: Themes = {
  dark: {
    color: "black",
  },
  light: {
    color: "white",
  },
};
// 定义Context的接收类型,也即传递的全局变量类型
export type ThemeContextProps = {
  theme: CSSProperties;
  toggleTheme?: () => void;
};
// 创建context
const ThemeContext = React.createContext<ThemeContextProps>({
  theme: themes.dark,
});
// 导出context
export default ThemeContext;
  1. Provider

定义全局变量

import ThemeContext, { themes } from './theme-context';
export const ThemeProvider: ReactElement = () => {
  const [state,setState] = useState<CSSProperties>({theme: themes.light})
  const toggleTheme = () =>
    setState(
      (state) =>
        ({
          theme: state.theme === themes.light ?
            themes.dark : themes.light,
        }))
  return (
    <ThemeContext.Provider value={{theme,toggleTheme}}>
      <Component/>
    </ThemeContext.Provider>
  )
}
  1. Consumer

子组件消费

export const Component:ReactElement = (props:Props) => {
  return (
    <ThemeContext.Consumer>
      {
        ({theme,toggleTheme})=><div style={theme} onClick={toggleTheme}/>
      }
    </ThemeContext.Consumer>
  )
}

参考:

(完全参考)react-redux-typescript-guide

TypeScript 配合 React 实践