TypeScript

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, 可以节省很多时间。