React 使用 TypeScript 的一些实践

1,024 阅读4分钟

组件状态属性声明

函数组件

属性声明

简单组件声明

类组件通过 React.FC<Props> 声明组件的属性

// test.tsx
import { FC } from 'react';

interface TestProps {
  val: string;
  onClick?: () => void;
}

const Test: FC<TestProps> = ({ val, onClick, children }) => {
  return <div></div>;
};

export default Test;

使用组件

import Test from './test';

// 以下用法是正常的,不会报错
<Test val="test" />
<Test val="test" onClick={() => {}} />
<Test val="test" onClick={() => {}}>
  <div className="text"></div>
</Test>

// 以下是错误的,会报错
<Test /> 缺失val
<Test val={1} /> val类型不对
<Test val="test" onClick={(val: string) => {}} /> onClick 属性不对
<Test val="test" onClick={(val: string) => {}} extra={'test'} />  多了额外属性

含子组件声明

// 例子举得不好,可以看下 Ant Design 的 Select 和 Select.Option
import { FC } from 'react';

interface TestItemProps {
  val: string;
  onClick: () => void;
}

const TestItem: FC<TestItemProps> = ({ onClick, val, children }) => {
  return <div></div>;
};

interface TestProps {
  list: string[];
  onItemClick: (idx: number) => void;
}

interface TestComponentProps extends FC<TestProps> {
  Item: typeof TestItem;
}

const Test: TestComponentProps = ({ list, onItemClick, children }) => {
  return <div></div>;
};

Test.Item = TestItem;

export default Test;

含子组件使用

import Test from './test';

const UseTest = () => {
  const list = ['a', 'b', 'c'];
  return (
    <Test onItemClick={() => {}} list={list}>
      {list.map((i, idx) => (
        <Test.Item onClick={() => {}} val={i}>
          {idx}
        </Test.Item>
      ))}
    </Test>
  );
};

状态声明

状态声明的时候,建议用范型显式标明属性的类型

const Test: TestComponentProps = ({ list, onItemClick, children }) => {
  const item = useState<string>('');
  const itemRef = useRef<string>(null);
  const handledList = useMemo<number[]>(() => list.map((i, idx) => idx), [list]);
  return <div></div>;
};

类组件

类组件通过 React.Component<Props, State> | React.PureComponent<Props, State> 声明组件的属性和状态

类组件声明状态和属性

import { Component } from 'react';

interface State {
  count: number;
}

interface Props {
  list: string[];
}

export default class ClassComponent extends Component<Props, State> {
  constructor(props: Props) {
    super(props);
    this.state = { count: 0 };
  }
}

// 使用
<ClassComponet list={['1']}></ClassComponet>;

常见的一些问题

Window 上面挂属性的方法

这个问题是 TypeScript 本身的问题,和 React 无关,不过因为经常遇到,直接在这里提一下

// 因为 __test_data__ 不是原生的 Window 上的属性,所以这样用是会报错的
window.__test_data__(
  // 大部分时候的解法
  window as any,
).__test_data__;
// 这样写没有任何问题,大部分时候也推荐这么写,可能数据结构比较简单,但是如果属性比较复杂,建议还是做一下声明,方便后续使用

// 假设
enum Enviroment {
  local,
  pre,
  prod,
}
interface TestData {
  index: number;
  item: string;
}

// 在项目根目录随便新建一个  index.d.ts 文件,
// 注意不能写 export interface Window
interface Window {
  env: Enviroment;
  __test_data__: TestData;
}

// 同样的,某些格式导出报错的问题,也可以用这个方式,index.d.ts 文件声明这些模块
declare module '*.css';
declare module '*.less';
declare module '*.scss';
declare module '*.sass';
declare module '*.styl';
declare module '*.stylus';
declare module '*.png';
declare module '*.jpeg';
declare module '*.jpg';
declare module '*.svg' {
  export function ReactComponent(props: React.SVGProps<SVGSVGElement>): React.ReactElement;
  const url: string;
  export default url;
}

JSDoc 生成属性说明

TypeScript 完整支持 JSDoc ,在 React 中可以使用 JSDoc 生成组件的属性说明,这样在提供公共组件的时候,用户在引用你的组件的时候,通过编辑器就能知道属性的注释是什么

// test.tsx
import { FC } from 'react';

// 注意不能是 //
interface TestProps {
  /** 组件的值 */
  val: string;
  /** 组件点击回调 */
  onClick?: () => void;
}

interface TestProps {
  /**
   * 组件的值
   */
  val: string;
  /** 组件点击回调 */
  onClick?: () => void;
}

const Test: FC<TestProps> = ({ val, onClick, children }) => {
  return <div></div>;
};

export default Test;

// 使用组件
<Test val={1} onClick={() => {}} />
当鼠标移到 val 上
(property) TestProps.val: string
组件的值
当鼠标移到 onClick 上
(property) TestProps.onClick?: () => void
组件点击回调

关于原生组件一些方法的声明

对于原生的 html 标签,触发 onClick 或者其他事件的时候,有时候需要禁止事件的一些冒泡,或者禁止默认行为,这时候需要使用到类似于原生事件的 stopPropagation 的方法,但是如果不熟悉这里怎么定义,只能把组件回调参数 event 定义为 any

const onClick = (e: any) => {
  e.stopPropagation();
};

这样写没有任何问题,不过没有任何语法提示就是了 其实 React 对各个 HTML 标签属性的类型声明做的非常好,当你在 div 声明一个 onClick 的时候,把鼠标移到上面,或者跳转到属性定义,就能看到 onClick 的函数声明

(property) React.DOMAttributes<HTMLDivElement>.onClick?: React.MouseEventHandler<HTMLDivElement> | undefined
<div onClick={onClick}></div>

因而,我们的函数声明可以直接用 React.MouseEventHandler<HTMLDivElement>,同时,这时候的回调参数 e 会被识别为 React.MouseEvent<HTMLDivElement, MouseEvent>,你可以随意使用 React 合成事件里面的方法

(parameter) e: React.MouseEvent<HTMLDivElement, MouseEvent>
const onClick: React.MouseEventHandler<HTMLDivElement> = e => {
  e.stopPropagation();
  e.preventDefault();
};

当然,知道 e 的类型,也可以自己写,这样写也完全没有问题,看自己习惯了。

(parameter) e: React.MouseEvent<HTMLDivElement, MouseEvent>
const onClick = (e: React.MouseEvent<HTMLDivElement>) => {
  e.stopPropagation();
  e.preventDefault();
};

以上三种都能解决原生方法类型不对的问题,更加推荐 React.MouseEventHandler<HTMLDivElement> 的写法,因为这个有比较完整的提示

React.memo 和 React.FC

这两个结合起来本身是没什么问题的,可以看下面的例子

interface DrawerProps {
  visible: boolean;
  onClose: () => void;
  closeWhenClickMask?: boolean;
  zIndex?: number;
}


const Drawer: React.FC<DrawerProps> = ({ visible, onClose, children }) => {};

export default React.memo(Drawer);

// 在别的组件使用
// 正常
<Drawer visible={visible} onClose={() => {}}></Drawer>
// 异常
<Drawer visible={visible} onClose={() => {}}>
  <div>121</div>
</Drawer>
Type '{ children: Element[]; visible: boolean; onClose: () => void; }' is not assignable to type 'IntrinsicAttributes & DrawerProps'.
  Property 'children' does not exist on type 'IntrinsicAttributes & DrawerProps'.ts(2322)

这里的异常表明 children 不是 Drawer 的属性,但是正常情况下,React.FC 会注入 & { children?: React.ReactNode } 这个属性,但是这里好像凭空消失了一样,尝试了很多方法,最后总结出几种写法

// 手动把 children 属性补上
interface DrawerProps {
  visible: boolean;
  onClose: () => void;
  closeWhenClickMask?: boolean;
  zIndex?: number;
  children?: React.ReactNode;
}

// 其他地方写法依旧

// 手动更改导出的组件类型
export default React.memo(Drawer) 改成
export default React.memo(Drawer) as React.FC<DrawerProps>;

// 第三种,和第二种写法其实没有差别,不过将 React.memo 换在上面包裹了,改变了导出组件的类型
const Drawer: React.FC<DrawerProps> = React.memo(({ visible, onClose, children }) => {});

export default Drawer;