TypeScript

上次修改时间:2021-06-04 11:01:21

Pro 中是用 TypeScript 来作为默认的开发语言,TypeScript 的好处已经无须赘述,无论是开发成本还是维护成本都能大大减少,是中后台开发的必选。这里分几个维度来聊一下 Pro 中对于 TypeScript 的最佳实践。

什么时候推荐用 type 什么时候用 interface ?

推荐任何时候都是用 type, type 使用起来更像一个变量,与 interface 相比,type 的特点如下:

  • 表达功能更强大,不局限于 object/class/function
  • 要扩展已有 type 需要创建新 type,不可以重名
  • 支持更复杂的类型操作

基本上所有用 interface 表达的类型都有其等价的 type 表达。在实践的过程中,我们也发现了一种类型只能用 interface 表达,无法用 type 表达,那就是往函数上挂载属性。

interface FuncWithAttachment {
  (param: string): boolean;
  someProperty: number;
}

const testFunc: FuncWithAttachment = {};
const result = testFunc('mike'); // 有类型提醒
testFunc.someProperty = 3; // 有类型提醒

定义接口数据

任何一个项目都离不开对数据和接口的处理,拼接数据和接口是形成业务逻辑也是前端的主要工作之一,将接口返回的数据定义 TypeScript 类型可以减少很多维护成本和查询 api 的时间。

在 Pro 推荐在 src/services/API.d.ts 中定义接口数据的类型,以用户基本信息为例:

declare namespace API {
  // 用户基本信息
  export type CurrentUser = {
    avatar?: string;
    name?: string;
    title?: string;
    group?: string;
    signature?: string;
    tags?: {
      key: string;
      label: string;
    }[];
    userid?: string;
    access?: 'user' | 'guest' | 'admin';
    unreadCount?: number;
  };
}

很多项目中是没有类型定义的,这里推荐 json2ts 等网站来自动转化。

在使用时我们就可以很简单的来使用, d.ts 结尾的文件会被 TypeScript 默认导入到全局,但是其中不能使用 import 语法,如果需要引用需要使用三斜杠。

export async function query() {
  return request<API.CurrentUser[]>('/api/users');
}

// props
export type UserProps = {
  userInfo: API.CurrentUser;
};

泛型

在业代码中开发时我们并不推荐大家写泛型,但是为了得到更好的 typescript 体验我们可能需要了解一下常用组件库的泛型提示,这里做个简单列举。

import ProForm from '@ant-design/pro-form';
import ProTable, { ActionType } from '@ant-design/pro-table';
import React, { useState, useRef } from 'react';

type DataType = {};

const Page = () => {
  // useState 的泛型会变成 state的类型
  const [state, setState] = useState<string>('');
  // useRef 的类型会被设置为 actionRef.current 的类型
  const actionRef = useRef<ActionType>();

  // click 使用 React.MouseEvent 加 dom 类型的泛型
  // HTMLInputElement 代表 input标签 另外一个常用的是 HTMLDivElement
  const onClick = (e: React.MouseEvent<HTMLInputElement>) => {};
  // onChange使用 React.ChangeEvent 加 dom 类型的泛型
  // 一般都是 HTMLInputElement,HTMLSelectElement 可能也会使用
  const onChange = (e: React.ChangeEvent<HTMLInputElement>) => {};
  return (
    <>
      {'ProForm 设置泛型可以约定 onFinish 等接口的参数类型'}
      <ProForm<DataType> />
      {`
        DataType 设置render 中行的类型,
        Params 是参数的提交类型
        ValueType 表示自定的 valueType 类型,ProTable 会自动进行合并
      `}
      <ProTable<DataType, Params, ValueType> />
      <input onClick={onClick} onChange={onChange} />
    </>
  );
};

定义一个组件的 n 种写法

const WrapComponent: React.FC<ExtendedProps> = (props) => {
  // return ...
};

export default WrapComponent;

// 或者
export default function (props: React.PropsWithChildren<SpinProps>) {
  // return ...
}

umi 常用类型

umi 在很多地方都帮助我们进行了封装,如果知道具体的类型可以减少很多 any。

页面相关

IRouteComponentProps 是被配置在 config.ts 中组件的 props 类型,其中带入了一些 react-router 相关的 props

export interface IRouteComponentProps<
  Params extends { [K in keyof Params]?: string } = {},
  Query extends { [K in keyof Query]?: string } = {}
> {
  children: JSX.Element;
  location: Location & { query: Query };
  route: IRoute;
  routes: IRoute[];
  history: History;
  match: match<Params>;
}

我们可以在页面中这样使用:

import React from 'antd';
import { IRouteComponentProps } from 'umi';

const Page: React.FC<IRouteComponentProps> = () => {
  return <Layout />;
};

为 Window 增加参数

前端开发很大程度上就是与 Window 打交道,有时候我们不得不给 Window 增加参数,例如各种统计的代码。在 TypeScript 中提供一个方式来增加参数。在 /src/typings.d.ts 中做如下定义:

interface Window {
  ga: (
    command: 'send',
    hitType: 'event' | 'pageview',
    fieldsObject: GAFieldsObject | string,
  ) => void;
  reloadAuthorized: () => void;
}

如果不想在 Window 中增加,但是想要全局使用,比如通过 define 注入的参数,我们通过 declare 关键字在 /src/typings.d.ts 注入。

declare const REACT_APP_ENV: 'test' | 'dev' | 'pre' | false;

这些样例都可以在 /src/typings.d.ts 中看到。

组件的类型

antd 是非常方便的一套 UI 库,为了更好的使用它,我们需要了解它的一些类型。

Form

Form 中的常用的类型很多,大部分都可以从 'antd/es/form 中导出,这里介绍几个最常用的。

antd@4 中使用 Form.useForm() 生成的 form 类型就是 FormInstance。FormItemProps 也是比较常用的类型,我们可以用这个类型来封装 FormItem , 增加自己的逻辑。

import { FormInstance, FormItemProps } from 'antd/es/form';

const [form] = Form.useForm();

//  保存 ref
const ref = useRef<FormInstance>();
ref.current = form;

由于 form 的多变性,form.getFieldsValue(); 返回的值都是 Store 类型,我们可以直接 as 为自己想要参数。

const user = form.getFieldsValue() as API.CurrentUser;

Table

这里推荐使用 ProTable,类型比较清晰,常用类型的示例。

import { ProColumns, ActionType } from '@ant-design/pro-table';

const columns: ProColumns<API.CurrentUser>[] = [
  {
    title: '姓名',
    dataIndex: 'name',
    hideInSearch: true,
  },
];

const actionRef = useRef<ActionType>();

export default <ProTable actionRef={actionRef} />;

另外 TablePaginationConfigTableRowSelection 比较常用,这两个都是泛型使用的时候要特别注意。

import { TablePaginationConfig } from 'antd/es/table/Table';
import { TableRowSelection } from 'antd/es/table/interface';

const pagination: TablePaginationConfig = {
  pageSize: 20,
  total: 2000,
  onChange: (current) => {},
};

const rowSelection: TableRowSelection = {
  selectedRowKeys: [],
  onChange: (keys, rows) => {},
};

一些小坑

React.ReactText[]

string[]|number[](string|number)[]并不相同,这种时候直接使用 React.ReactText[] 就好了。

React.forwardRef

如果我们使用 function 组件,可能会报错 ref 找不到,这时候我们就需要使用 React.forwardRef,但是要注意的是 类型也要做相应的修改。

- React.FC<CategorySelectProps>
+ React.ForwardRefRenderFunction<HTMLElement, CategorySelectProps>

动态增加

有时候我需要对一个 Object 的 key 进行动态的更新,为了方便我们可以对 key 设置为 any,这样就可以使用任何 key,多余 JSON.parse

type Person = {
  name: string;
  age?: number;
  [propName: string]: any;
};

值可以为 null 或 undefined

在 3.8 中已经很简单了,obj?.xxx 即可。

某个库不存在 typescript 的定义

我们可以直接将其定义为 any。

declare module 'xxx';

import xxx from 'xxx';

@ts-ignore

有些时候类型错误是组件的,但是看起来非常难受。会一直编译报报错,这里就可以使用 @ts-ignore 来暂时忽略它。

// @ts-ignore
xxxx;

TypeScript 毕竟是一个标注语言,在需要使用 any 的时候不必吝于使用 any,在遇到动态性比较强的代码,不妨使用 as unknown as XXX, 可以节省很多时间。