React中优雅的使用Typescript

324 阅读5分钟

背景

React是前端编写组件的方式, Typescript为组件提供了强类型的类型提示和检查, 尤其是对于组件属性类型的提示, 可以极大帮助组件的使用者快速准确的提供属性值.

因此极力推荐使用Typescript编写React组件.

如何在React中优雅的使用Typescript

在React使用Typescript主要集中在两个方面:

  1. 如何使用Typescript编写和使用React组件
  2. 如何使用Typescript引用React库自身的API

在深入介绍之前, 有必要对React自带的常用Typescript类型做一介绍.

React中的类型

React.JSX.Element vs React.ReactNode

  • React.JSX.Element

    是 React.createElement()方法的返回值的类型, 也就是所有 React元素的类型

  • React.ReactNode

    是 React 可以渲染的任何东西的类型(React.JSX.Element, number, string, boolean, bigint, symbol, null or undefined)

    注意: 对象不是合法的组件返回值.

React.ElementType vs React.ComponentType

  • React.ElementType 所有组件的类型, 包括原生组件,类组件以及函数组件的类型
  • React.ComponentType 类组件和函数组件的类型, 不含原生组件

Interface vs Type alias

  • React 组件的属性使用 Type alias, 因为 Type alias 有更多的约束以便实现一致性.
  • 公共 API 定义使用 Interface, 以便 API 使用者通过声明合并扩展它的定义.

React 组件属性类型

通常, React 组件渲染的根组件是React的原生组件, 此时, 组件属性可以分为自身的属性和需要传递给原生组件的属性.

最佳实践: 将组件自身属性和继承属性分开定义. 这样做的好处是: 当其它组件需要基于基础组件的属性扩展时, 只需继承基础组件的自身属性即可而不必继承原生组件的属性, 因为扩展的组件可能会使用不同的根组件.

看一个例子:

export type MyComponentOwnProps = {
  defaultValue?: string;
  value?: string;
  onChange?: (val: string) => void;
}

type MyComponentProps = MyComponentOwnProps &
  Omit<React.ComponentPropsWithoutRef<"div">, keyof MyComponentOwnProps>;
  
export const MyComponent = forwardRef<HTMLDivElement, MyComponentProps>( (props, ref) => {
   // ...
   return (
     <div>
       //...
     </div>
   );
});

该例子中只导出了自身属性, 合并后的最终属性则作为私有属性不导出.

这里也使用了一个技巧: Omit<React.ComponentPropsWithoutRef<"div">, keyof MyComponentOwnProps> 会自动排除掉组掉自身属性与原生组件属性同名的属性.

这个例子可以作为编写组件的模板使用.

单个属性的类型

export interface MyComponentProps {
  message: string;
  count: number;
  disabled: boolean;
  /** 类型化的数组 */
  names: string[];
  /** 字符串字面量枚举 */
  status: "waiting" | "success";
  /** 类型化的对象 */
  obj: {
    id: string;
    title: string;
  };
  /** 类型化的对象数组 */
  objArr: {
    id: string;
    title: string;
  }[];
  /** 任何非原生类型,但不可访问任何属性 (不常用) */
  obj2: object;
  /** 与上面等价 (不常用)*/
  obj3: {};
  /** 带有任意属性的对象 */
  dict1: {
    [key: string]: MyTypeHere;
  };
  /** 与上面的等价 */
  dict2: Record<string, MyTypeHere>;
  /** 无参数无返回值的函数, 如自定义的事件处理器 */
  onClick: () => void;
  /** 带参数无返回值的函数, 如自定义的事件处理器 */
  onChange: (id: number) => void;
  /** 带参数无返回值的函数, 如原生的事件处理器 */
  onChange: (event: React.ChangeEvent<HTMLInputElement>) => void;
  /** 带参数无返回值的函数, 如原生的事件处理器 */
  onClick(event: React.MouseEvent<HTMLButtonElement>): void;
  /** 可选属性
   *
   * @default a
   */
  optional?: OptionalType;
  /** `useState()`返回的设置函数作为属性类型 */
  setState: React.Dispatch<React.SetStateAction<MyTypeHere>>;
  /** useReducer()返回的dispatch作为属性类型 */
  dispatch: React.Dispatch<MyTypeHere>;
  /** 组件类型 */
  as: React.ElementType<MyTypeHere>;
  
  /** 从其它组件的单个属性中扩展 */
  slots?: OtherComponentOwnProps["slots"] & {
    dropdown?: string;
  };

  /** style属性 */
  style: React.CSSProperties; // 可以传递给style属性.
  /** children属性 */
  children: React.ReactNode; // 可被React渲染的任何单个东西
  children: [React.ReactNode, React.ReactNode]; // 孩子为指定个数的元组
  children: React.ReactNode[]; // 可被React渲染的任何东西的数组
  children: React.JSX.Element; // 单个React组件元素, 不能是true,false,null,undefined,字符串或其它非组件
  children: [React.JSX.Element, React.JSX.Element]; // 孩子为指定个数的元组
  children: React.JSX.Element[]; // React元素数组
  children: (foo: string) => ReactNode; // 函数作为孩子
}

附属组件

有时, 想在主组件上带附属组件

首先按照按上面的规则定义, 但组件名使用_作为后缀(这只是作为约定), 但不需要到导出, 在文件最后添加:

如果想命名导出:

import { Other } from "./Other
// ...

export const MyComponent = Object.assign(MyComponent_, { Other });

如果想默认导出:

import { Other } from "./Other
// ...
export default Object.assign(Grid_, { Col });

泛型组件

import { ReactNode, useState } from "react";

interface Props<T> {
  items: T[];
  renderItem: (item: T) => ReactNode;
}

function List<T>(props: Props<T>) {
  const { items, renderItem } = props;
  const [state, setState] = useState<T[]>([]); // You can use type T in List function scope.
  return (
    <div>
      {items.map(renderItem)}
      <button onClick={() => setState(items)}>Clone</button>
      {JSON.stringify(state, null, 2)}
    </div>
  );
}

使用时会进行自动类型推断:

ReactDOM.render(
  <List
    items={[1, 2]} // 会根据items的属性值推断T为number
    renderItem={(item) => (
      <li key={item}>
        {/* Error: Property 'toPrecision' does not exist on type 'string'. */}
        {item.toPrecision(3)}
      </li>
    )}
  />,
  document.body
);

也可以在 TSX 中明确指定 T 类型

ReactDOM.render(
  <List<number>
    items={["a", "b"]} // Error: Type 'string' is not assignable to type 'number'.
    renderItem={(item) => <li key={item}>{item.toPrecision(3)}</li>}
  />,
  document.body
);

React自身的API的引用

React Hooks

useState

不可空的基本类型:

const [val, setVal] = React.useState(false); 

val 为 boolean类型, setVal 为只接受一个boolean参数的void函数.

初始值为空的对象类型

const [user, setUser] = React.useState<IUser | null>(null);

useReducer

可以使用可辨识联合类型定义 actions, 方便在 reducer 中的 switch 判断.

type ACTIONTYPE =
  | { type: "increment"; payload: number }
  | { type: "decrement"; payload: string };

function reducer(state: typeof initialState, action: ACTIONTYPE) {
    switch (action.type) {
        case "increment":
            return { count: state.count + action.payload };
        case "decrement":
            return { count: state.count - Number(action.payload) };
        default:
            throw new Error();
    }
}

useRef

React 中 useRef 返回的引用可以是只读的,也可以是可变的. 即便是只读的, 也需要判断是否为空. 因为有可能你使用了条件渲染, 而 ref 的元素可能不一定渲染, 或者忘了赋值 ref.

const divRef = useRef<HTMLDivElement>(null); // 返回值类型为: RefObject, 它是只读. 只能赋值给ref属性. 如果你不知道元素的类型, 一旦你赋值给了ref, VsCode会提示你所需的类型.

const intervalRef = useRef<number | null>(null); // 返回值类型为: MutableRefObject, 它是可变的. 可能为空, 代码必须判断是否为空, 用于组件内的自定义缓存.

const intervalRef = useRef<HTMLDivElement>(null!); // 返回值类型为: MutableRefObject, 它是可变的. 强制断言为不为空, 代码可以不必判断,只要确保 divRef.current不为null. 用于组件内的自定义缓存.

const intervalRef = useRef(1); // 返回值类型为: MutableRefObject, 它是可变的. 自动推断不可为空, 用于组件内的自定义缓存. 需要确保 divRef.current不为null

React Context

const StoreContext = React.createContext<StoreContextState | undefined>(
  undefined
);

export function useStore() {
    const store = React.useContext(StoreContext);
    if (store === undefined) throw new Error("useStore must be inside a Provider with a value");
    return store;
}