TypeScript With React

339 阅读7分钟

组件状态和属性声明

函数组件

函数组件通过 React.FC<Props>useState<State> 定义属性和状态

import { FC } from 'react';

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

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

export default Test;

类组件

类组件通过 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>;

声明静态资源

TypeScript 认为导入的头文件必须是一个模块,如果它没法识别出导入的文件路径是一个模块,它会报错,所以它不能像 js 那种直接把样式表或者图片资源这种静态资源导入,会报错,解决方案是声明这种路径是一个模块,涉及到覆盖范围一般是在项目根目录下声明就好,比较好的实践是 src/types.d.ts 或者 src/typings.d.ts,标明是 d.ts 文件,有个好处就是会触发 eslint 默认不检测 d.ts 的文件

样式表

普通样式表,只要导入到组件就能使用

// Cannot find module './index.less' or its corresponding type declarations.
import './index.less';

// at src/typings.d.ts
// 把主流的样式表格式都加进去
declare module '*.css';
declare module '*.sass';
declare module '*.scss';
declare module '*.less';
declare module '*.styl';
declare module '*.stylus';

css-module

理论上按上面方式配置之后,css-module 方式导入也不会有报错,但是可以单独针对 css-module 引入的样式表模块做一下处理

// 在配置了 '*.less' 后,styles 是 any,没任何问题
import styles from './index.module.less';

// at src/typings.d.ts
// 把主流的样式表格式都加进去
declare module '*.module.css' {
  const classes: { [key: string]: string };
  export default classes;
}
declare module '*.module.sass' {
  const classes: { [key: string]: string };
  export default classes;
}
declare module '*.module.scss' {
  const classes: { [key: string]: string };
  export default classes;
}
declare module '*.module.less' {
  const classes: { [key: string]: string };
  export default classes;
}
declare module '*.module.styl' {
  const classes: { [key: string]: string };
  export default classes;
}
declare module '*.module.stylus' {
  const classes: { [key: string]: string };
  export default classes;
}
// 注意 module 声明应该在上面,ts 模块的匹配是优先匹配,如果符合 '*.less' 就不会再往下匹配了
declare module '*.css';
declare module '*.sass';
declare module '*.scss';
declare module '*.less';
declare module '*.styl';
declare module '*.stylus';

// 这样配置之后 styles 就是一个对象,类型是 { [key: string]: string }

图片和字体静态文件

这样类型的静态文件也类似,直接声明就是,以下两种方式都可以

// at src/typings.d.ts
// 声明导入的 '.png' 模块是一个字符串
declare module '*.png' {
  const value: number;
  export default value;
}
// 没有显式表明模块导出,就是 any
declare module '*.jpg';
declare module '*.jpeg';

window 上声明变量

不推荐在 window 上挂属性,不过有些变量是 html 主文档注入的,因而有些时候需要读 window 变量,但是直接读会报错,比如白龙马注入的环境变量

// Property 'ENV_TYPE' does not exist on type 'Window & typeof globalThis'.
window.ENV_TYPE;

// 在 src/typings.ts
// 注意不能写 export,利用 TypeScript 自动声明合并的能力
interface Window {
  ENV_TYPE: 'prod' | 'pre' | 'local';
}

// 当前夸克和白龙马下面注入的变量主要有这几种
interface Window {
  ENV_TYPE: 'prod' | 'pre' | 'local';
  ucapi: {
    invoke: Function;
  };
  __itrace: any;
}

常见的类型声明

获取一个变量的类型

TypeScript 中获取一个变量的类型可以使用 typeof 操作符,这种对于导入一个变量,要获取变量的类型十分方便

  • 这里有两种情况,一是可以顺便把变量的类型也导入
  • 但是变量的类型,不一定作为一个类型或者接口导出,例如下面的 case,要获取一个变量的类型,typeof 很有用
interface User {
  id: number;
  name: string;
}

export const user = { id: 1, name: 'test' };

import { user } from './';
// error
// 'user' refers to a value, but is being used as a type here. Did you mean 'typeof user'?
const user2: user = { id: 2, name: 'test2' };
// ok
const user2: typeof user = { id: 2, name: 'test2' };

获取一个函数的返回类型

和获取变量类型一样,获取函数类型同样也存在于导入的函数中,可以方便的获取函数返回类型,在函数二次导出(例如 React 中的 useImperativeHandleuseRef ),只想获取函数的某个属性的类型特别有用

interface ProductInfo {
  id: number;
  price: number;
}

const usePay = () => {
  const [productList, setProductList] = useState<ProductInfo[]>([]);
  const buy = useCallback((id: number) => {}, []);
  return { productList, buy };
};

// 在别的组件里面使用 `usePay` ,能正常识别状态
// const list: ProductInfo[]
const [list] = useState<ReturnType<typeof usePay>['productList']>([]);

useImperativeHandle 和 useRef 类型声明

  • useRef 引用 HTML 元素

    const inputRef = useRef<HTMLInputElement>(null)
    
    // 使用
    inputRef.current?.blur()
    
    //
    <input ref={inputRef} />
    
  • 组件 forwardRefuseImperativeHandle 类型声明

    function forwardRef<T, P = {}>(render: ForwardRefRenderFunction<T, P>): ForwardRefExoticComponent<PropsWithoutRef

    & RefAttributes>;

    forwardRef 的签名有两个范型,T 表示组件暴露的方法,P 是组件的属性

    interface MessageModalProps {
      message: string;
    }
    export interface MessageModalMethods {
      open: () => void;
      close: () => void;
    }
    
    const MessageModal = forwardRef<MessageModalMethods, MessageModalProps>(
      ({ message }, ref) => {
        const [visible, setVisible] = useState<boolean>(false);
    
        useImperativeHandle(ref, () => ({
          open: () => setVisible(true),
          close: () => setVisible(false),
        }));
      }
    );
    
  • useRef 引用 React 组件

    import MessageModal from './message-modal';
    import type { MessageModalMethods } from './message-modal';
    
    const messageModalRef = useRef<MessageModalMethods>(null);
    
    // 使用
    messageModalRef.current?.open();
    messageModalRef.current?.close();
    
    <MessageModal message={message} ref={messageModalRef} />;
    

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>;

异常的是组件传了 children 的时候,报了下面的报错

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)

// 异常
<Drawer visible={visible} onClose={() => {}}>
  <div>121</div>
</Drawer>

这里的异常表明 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;

"jsx": "react-jsx" 的问题

"jsx": "react-jsx"TypeScriptv4.1 之后加的一个属性,主要是给 React v17 使用的,如果不是 Ts 4.1+,这个属性会报错,如果碰到这个报错,请把 "typescript": "^4.1.0" 加到你的 package.json 里面,重新安装一次依赖,就不会报错了

Type 'Parameters' must have a 'Symbol.iterator' method that returns an iterator.

这个问题就有点怪了,Parameters<T> 返回的就是个数组,但是这里居然报错了,可以用 [...Parameters<T>] 包裹一下

/**
 * 异步函数重试
 * @param retryTimes 重试次数
 * @param fn 需要重试执行的函数
 * @param args 函数的参数
 * @returns
 */
export const fnRetry = <T extends (...args: any) => any>(
  retryTimes: number,
  fn: T,
  ...args: [...Parameters<T>]
) => {
  let times = retryTimes;
  return new Promise<ReturnType<T>>((resolve, reject) => {
    const run = () => {
      fn(...args)
        .then((r: any) => resolve(r))
        .catch((err: any) => {
          if (times > 0) {
            times -= 1;
            run();
          } else {
            reject(err);
          }
        });
    };
    run();
  });
};