codesandbox 链接

组件定义

以一个输入框组件为例,这里分别展示类组件和函数组件上关于ts的实践

类组件

  1. import React from 'react';
  2. export interface IInputProps {
  3. value?: string;
  4. defaultValue?: string;
  5. onChange?: (value?: string) => void;
  6. }
  7. interface IInputState {
  8. innerValue: string;
  9. }
  10. class Input extends React.Component<IInputProps, IInputState> {
  11. // 设置默认props
  12. // 这里的类型需要设置成Partial,因为有些非空类型在默认props上不是必须的
  13. static defaultProps: Partial<IInputProps> = {
  14. defaultValue: '',
  15. };
  16. // 此处声明类组件的状态
  17. state: IInputState = {
  18. // 组件内初始状态由props上传入的defaultValue初步决定
  19. // 若没有传入defaultValue,再自行使用内部决定的初始值
  20. innerValue: this.props.defaultValue ?? '',
  21. };
  22. componentDidUpdate(prevProps: IInputProps, prevState: IInputState) {
  23. // 使用 componentDidUpdate 时需要声明相应的 props 和 state 类型
  24. }
  25. /**
  26. * 通过判断是否有在props上传入value字段确认是否为受控组件
  27. */
  28. get isControlled() {
  29. return 'value' in this.props;
  30. }
  31. /**
  32. * 当前展示出来的内容
  33. * 如果是受控模式,展示外部value
  34. * 如果是非受控模式,展示内部innerValue
  35. */
  36. get displayValue() {
  37. return this.isControlled ? this.props.value : this.state.innerValue;
  38. }
  39. handleChangeValue = (value?: string) => {
  40. // 如果是非受控组件,那么需要维持内部的状态
  41. if (!this.isControlled) {
  42. this.setState({ innerValue: value ?? '' });
  43. }
  44. // 不论当前组件是否受控/非受控,都需要保证触发props.onChange
  45. this.props.onChange?.(value);
  46. };
  47. // 此处的类型声明,可以移动到 <input /> 标签的 onChange 属性上,会有相应的类型提示
  48. handleInputChange: React.InputHTMLAttributes<HTMLInputElement>['onChange'] = (
  49. event,
  50. ) => {
  51. this.handleChangeValue(event.target.value);
  52. };
  53. render() {
  54. return (
  55. <input value={this.displayValue} onChange={this.handleInputChange} />
  56. );
  57. }
  58. }
  59. export default Input;

在类组件上使用ts,主要注意点包括以下方面:

  1. 继承自React.Component需要声明props和state的类型,通常组件的IProps需要导出,但是IState一般不导出image.png打开React.Component的类型声明,你可以发现这个泛型的默认类型是{},也就是说,如果你的类组件不声明任何泛型,那么ts直接就不允许你在this.propsthis.state上操作任何属性image.png最坏的打算就是写.tsx文件时extends React.Component<any, any>
  2. 类组件的defaultProps可以以static属性设置,但是需要手动声明类型,因为组件的默认props只是IProps的局部配置,所以这里使用一个ts工具类型Partialimage.png
  3. 类组件的state可以以类字段的方式声明,此时需要自己手动去标注类型是IStateimage.png也可以使用constructor去声明,但是却需要声明constructorprops的类型image.png不论使用哪一种方式,都避不开需要手动声明类型,React.Comopnent上的泛型并没有智能到可以推断构造函数时的state类型
  4. 使用一些生命周期函数时,有些地方的类型避免不了需要自己去声明,例如使用componentDidUpdate需要自己声明props和state的类型image.png
  5. 使用其他组件的props时,声明类型可以通过代码提示去实现,例如本例子中的<input />标签的onChange函数,可以通过鼠标悬浮到上面即可获得代码提示image.png记得多导出IProps,它很有用,当使用导出组件的时候,一个props上的字段类型,可以通过IProps计算得出image.png

    函数组件

    ```tsx import React from ‘react’;

export interface IInputProps { value?: string; defaultValue?: string; onChange?: (value?: string) => void; }

// 使用React.VFC声明箭头函数的类型是React的函数组件 const Input: React.VFC = (props) => { const { value, defaultValue, onChange } = props;

// 使用React.useState声明内部状态,通过泛型声明类型 const [innerValue, setInnerValue] = React.useState( defaultValue ?? ‘’, );

const isControlled = ‘value’ in props;

const displayValue = isControlled ? value : innerValue;

const handleChangeValue = (value?: string) => { if (!isControlled) { setInnerValue(value ?? ‘’); } onChange?.(value); };

const handleInputChange: React.InputHTMLAttributes< HTMLInputElement

[‘onChange’] = (event) => { handleChangeValue(event.target.value); };

return ; };

// 由于声明了类型是React.VFC,可以识别出defaultProps的类型 Input.defaultProps = { defaultValue: ‘’, };

export default Input;

  1. 在函数组件上使用ts,要点如下:
  2. 1. 必须声明函数组件的类型是`React.FC``React.VFC`FCVFC的差异仅仅在于是否有在`props`上设置`{ children?: React.ReactNode }`![image.png](https://cdn.nlark.com/yuque/0/2021/png/680068/1626945392913-c89d7f47-a644-4b72-9acc-b4ca71b74dd7.png#clientId=ufb981edd-c518-4&crop=0&crop=0&crop=1&crop=1&from=paste&height=408&id=u14041662&margin=%5Bobject%20Object%5D&name=image.png&originHeight=408&originWidth=975&originalType=binary&ratio=1&rotation=0&showTitle=false&size=92410&status=done&style=none&taskId=uf1d3bfd6-3c33-4902-8641-66b28978f4c&title=&width=975)正是这里的`PropsWithChildren`包裹的props类型,为我们的props设置了children类型,本例子中的Input组件,由于类型是`React.VFC`,没有children类型声明,如果设置children将会报错![image.png](https://cdn.nlark.com/yuque/0/2021/png/680068/1626945550507-21d78afa-1f07-4034-bd2e-700c341b15ed.png#clientId=ufb981edd-c518-4&crop=0&crop=0&crop=1&crop=1&from=paste&height=377&id=ue670285e&margin=%5Bobject%20Object%5D&name=image.png&originHeight=377&originWidth=1657&originalType=binary&ratio=1&rotation=0&showTitle=false&size=78263&status=done&style=none&taskId=u0c47880f-677f-416e-bc09-e1d4b4e338b&title=&width=1657)关于具体要使用FC还是VFC,主要看应用场景
  3. 1. `React.useState`可以通过泛型声明内部状态的类型,此外也可以通过类型推导![image.png](https://cdn.nlark.com/yuque/0/2021/png/680068/1626945705978-c3a5ba46-78af-475f-82c8-d51aacc43c19.png#clientId=ufb981edd-c518-4&crop=0&crop=0&crop=1&crop=1&from=paste&height=183&id=u6277544b&margin=%5Bobject%20Object%5D&name=image.png&originHeight=183&originWidth=1030&originalType=binary&ratio=1&rotation=0&showTitle=false&size=47656&status=done&style=none&taskId=u68b1d82f-79ec-42ba-b87b-64fdaccad3f&title=&width=1030)
  4. 1. 声明为`React.FC`类型的函数组件,还可以获得`defaultProps` `displayName` 属性的类型提示![image.png](https://cdn.nlark.com/yuque/0/2021/png/680068/1626945856845-ad50f5f5-0f20-4f60-afd7-08e4f2c3e69a.png#clientId=ufb981edd-c518-4&crop=0&crop=0&crop=1&crop=1&from=paste&height=180&id=ube3f0951&margin=%5Bobject%20Object%5D&name=image.png&originHeight=180&originWidth=691&originalType=binary&ratio=1&rotation=0&showTitle=false&size=26015&status=done&style=none&taskId=ucfd43f52-87ab-4791-82d9-50516ba4af5&title=&width=691)![image.png](https://cdn.nlark.com/yuque/0/2021/png/680068/1626945917480-39d5eb36-4864-4232-b66d-4b7fd64537af.png#clientId=ufb981edd-c518-4&crop=0&crop=0&crop=1&crop=1&from=paste&height=239&id=udf5b5fef&margin=%5Bobject%20Object%5D&name=image.png&originHeight=239&originWidth=918&originalType=binary&ratio=1&rotation=0&showTitle=false&size=51512&status=done&style=none&taskId=ucb1c588f-cf1e-496a-98f8-9c26157ed08&title=&width=918)可以看出一个组件props上的`defaultProps`应该是`Partial`类型工具计算出来的,正如我们在类组件上使用的一样
  5. <a name="eLxjN"></a>
  6. #### 函数泛型组件
  7. 除了可以用箭头函数来声明函数组件以外,使用普通的`fucntion`也可以声明组件,但是却没有了`React.FC`ts的类型提示,但这不是说明了`function`声明的组件完全是`React.FC`的下位替代,当需要使用到泛型组件时,就避不开需要使用`function`声明。
  8. ```tsx
  9. import React from 'react';
  10. interface IOptionProps<T> {
  11. value: T;
  12. children: string;
  13. }
  14. function Option<T>(props: IOptionProps<T>) {
  15. return null;
  16. }
  17. interface IBaseOption<T> {
  18. value: T;
  19. label: string;
  20. }
  21. interface IBaseSelectProps<T> {
  22. onChange?: (value?: T) => void;
  23. value?: T;
  24. defaultValue?: T;
  25. }
  26. interface ISelectPropsWithOptions<T> extends IBaseSelectProps<T> {
  27. options: IBaseOption<T>[];
  28. }
  29. interface ISelectPropsWithChildren<T> extends IBaseSelectProps<T> {
  30. children: React.ReactNode;
  31. }
  32. type ISelectProps<T> = ISelectPropsWithOptions<T> | ISelectPropsWithChildren<T>;
  33. // 使用ts函数重载声明两种允许的props类型
  34. function Select<T>(props: ISelectPropsWithOptions<T>): React.ReactElement;
  35. function Select<T>(props: ISelectPropsWithChildren<T>): React.ReactElement;
  36. function Select<T>(props: ISelectProps<T>) {
  37. let options: IBaseOption<T>[] = [];
  38. // 使用类型防卫来识别不同props类型
  39. if ('children' in props) {
  40. const list =
  41. React.Children.map(props.children, (child) => {
  42. // 类型防卫;return后面的语句可以保证child类型必定是 ReactElement
  43. if (!React.isValidElement<IOptionProps<T>>(child)) {
  44. return;
  45. }
  46. if (child.type === Option) {
  47. return {
  48. value: child.props.value,
  49. label: child.props.children,
  50. } as IBaseOption<T>;
  51. }
  52. }) ?? [];
  53. if (list.length > 0) {
  54. options = list;
  55. }
  56. } else if ('options' in props) {
  57. options = props.options;
  58. }
  59. const [innerValue, setInnerValue] = React.useState<T | undefined>(
  60. props.defaultValue,
  61. );
  62. const isControlled = 'value' in props;
  63. const displayValue = isControlled ? props.value : innerValue;
  64. const handleChangeValue = (value?: T) => {
  65. if (!isControlled) {
  66. setInnerValue(value);
  67. }
  68. props.onChange?.(value);
  69. };
  70. const handleSelectChange: React.SelectHTMLAttributes<
  71. HTMLSelectElement
  72. >['onChange'] = (event) => {
  73. handleChangeValue(event.target.value as any);
  74. };
  75. return (
  76. <select
  77. // @ts-ignore
  78. value={displayValue}
  79. onChange={handleSelectChange}
  80. >
  81. {options.map((item) => (
  82. // 此处由于有约束 select 的 value 必须是 string 或 number
  83. // 由于使用了泛型类型而没有约束必须是 string 或 number
  84. // 为了展示用例,此处特意忽略类型检查
  85. // @ts-ignore
  86. <option key={item.value} value={item.value}>
  87. {item.label}
  88. </option>
  89. ))}
  90. </select>
  91. );
  92. }
  93. const ComplexedSelect = Object.assign(Select, { Option });
  94. export default ComplexedSelect;

image.png在组件后加上尖括号即可手动确定泛型,本处若声明为string,则设置1是number会报错image.png另一种确认泛型的方法是使用类型推导,如果预先设置options的value类型,那么T会被推导为number类型image.png
像本处的value设置成string类型,直接就识别报错了

使用函数组件的重载,可以限定传入的props类型,本用例中传入的optionschildren是水火不容的,如果传入其中一个再传入其他另一个就会报错
image.png使用函数重载可以保证我们想要的props是正好数量的字段,不会多也不会少

所有组件的类型

React.ComponentType<P>表示类组件和函数组件的联合定义

  1. type ComponentType<P = {}> = ComponentClass<P> | FunctionComponent<P>;

以上类型定义拷贝自React的类型定义,这个类型在写HOC时很有用

  1. function withState<P extends WrappedComponentProps>(
  2. WrappedComponent: React.ComponentType<P>
  3. ) {
  4. // ...
  5. // 返回处理后的结点
  6. }

forwarding refs

转发组件内的ref,就是使用React.useRef来获得JSX的引用,并且进行操作

  1. import { Modal } from 'antd';
  2. import React from 'react';
  3. export interface IMyModalRef {
  4. open: () => void;
  5. close: () => void;
  6. }
  7. export interface IMyModalProps {}
  8. const MyModal = React.forwardRef<IMyModalRef, IMyModalProps>((props, ref) => {
  9. const [visible, setVisible] = React.useState(false);
  10. React.useImperativeHandle(ref, () => ({
  11. open() {
  12. setVisible(true);
  13. },
  14. close() {
  15. setVisible(false);
  16. },
  17. }));
  18. return (
  19. <Modal
  20. visible={visible}
  21. title="forwarding refs"
  22. onCancel={() => {
  23. setVisible(false);
  24. }}
  25. >
  26. <div>modal</div>
  27. </Modal>
  28. );
  29. });
  30. export default MyModal;

使用时,在React.useRef<T>上将刚刚导出的IMyModalRef泛型声明

  1. import { Button } from 'antd';
  2. import 'antd/dist/antd.css';
  3. import React from 'react';
  4. import MyModal, { IMyModalRef } from './components/forwarding-refs/MyModal';
  5. const App: React.VFC = () => {
  6. const modalRef = React.useRef<IMyModalRef>(null);
  7. return (
  8. <div>
  9. <MyModal ref={modalRef} />
  10. <div>
  11. <Button
  12. onClick={() => {
  13. modalRef.current?.open();
  14. }}
  15. >
  16. OPEN MODAL
  17. </Button>
  18. </div>
  19. </div>
  20. );
  21. };
  22. export default App;

使用React.forwardRef少数地方有奇效,但是不是很推荐使用这个api,因为这会显得不是很符合React的哲学—“修改props/state,自动更新,数据流自上而下”,推荐用法是大部分地方遵守React哲学,少数地方可以下克上,针对特殊的地方可以使用转发ref

类组件和函数组件的差异

可以说如果要使用ts来书写react,类组件完全毫无优势可言,但是class上使用泛型会比function上简单很多,书写泛型组件的时候我更加推荐使用class组件,但是class组件却做不到重载,例如ant-design的Select组件无法区分optionschildren是否同时传入,即使两个props都写,ant-design的ts检查也识别不出来。

这里推荐ts在react中的实践就是,80%使用React.FC/React.VFC,FC的预置类型定义可以帮助你做简单的类型检查,10%的场景用function,泛型组件、props重载都需要function组件才能做到,10%的场景使用class组件,一些难以书写的泛型组件,还是得使用class来帮忙。

不好的实践

不要直接使用export default导出函数组件

  1. // Bad
  2. export default (props: IProps) => {
  3. return (
  4. <div></div>
  5. );
  6. }
  7. // Bad
  8. export default function(props: IProps) {
  9. return (
  10. <div></div>
  11. );
  12. }
  13. // Better
  14. export default function Foo(props: IProps) {
  15. return (
  16. <div></div>
  17. );
  18. }

如果非要这么做,请使用命名的function进行定义

使用前两种方式导出的组件在React Inspector查看时会显示为Unknown,如果是在storybook则会显示成No Display Name