提效保质:使用React泛型组件

在一次功能上线中,由于接口字段变动没有及时同步,导致出现问题。如何避免?

强类型的Table组件

动机与目的

ant design 的 Table 组件中,column 的 dataIndex 属性是列数据在数据项中对应的 key,为了支持 a.b.c、a[0].b.c[1] 这样的嵌套写法,所以是string类型的,这也是bug的根源:无法与数据的字段“绑定”,当接口变动时,IDE不会报错,而是导致运行时错误。同时render函数中的value参数给出any,也不能利用接口中已有的类型信息。

因此对Table组件进行了一层“包装”,实现为一个泛型组件,传入字段类型后可以约束columns的写法。内部进行一个简单的翻译,旨在完全兼容原本的Table组件(以及​ColumnProps​、​TableProps​)

实现思路

核心部分:给columns一个泛型类型,自动推断出需要填写的字段及其类型,并且可以让render中的value提示对应的type

新的column definition直接省去了dataIndex,用Col的键名来直接获得

interface Col<K extends keyof T, T> extends Base<T>{
    render?: (value: T[K], record: T, index: number) => React.ReactNode;
    ... // other ColumnProps
}

export type Cols<T> = {
    [K in keyof T]?: Col<K, T>;
};

对于需要自定义的column,单独书写,并且可以指定renderOrder

interface CustomCol<T> extends Base<T>{
    title: string;
    renderOrder?: number;
    render: (record: T, index: number) => React.ReactNode;
}

export interface CustomCols<T> {
    [col: string]: CustomCol<T>;
}

这是一个wrapper,​genColumns​函数用来将我们的columns转换成ant design的​ColumnProps​,rowKey也是强类型的,确保设置了合适的rowKey

interface Props<T>{
    data: T[];
    columns: Cols<T>;
    customCols?: CustomCols<T>;
    rowKey: keyof T | ((record: T, index: number) => string);
    ... // inheritance from TableProps
}
function genColumns<T>(cols: Cols<T>, customCols?: CustomCols<T>): ColumnProps<T>[] {...}

export const XTable = function <T>(props: Props<T>) {
    return (
        <Table
            {...props}
            columns={genColumns(props.columns, props.customCols)}
            dataSource={props.data}
            rowKey={props.rowKey as string}
        />
    );
};

Usage

interface IData {
    Id: number;
    Name: string;
    Desc: string;
}

const columns: Cols<IData> = {
    // 这里会自动推断IData里的key,作为这里的key
    Id: {
        title: '序号'
    },
    Name: {
        title: '姓名'
    },
    Desc: {
        title: '描述',
        // render中的value会被自动推断为typeof IData["Desc"],即string
        render: value => <span>{value}</span>
    },
};

// 自定义未在IData中出现的column,默认排在末尾,可手动指定renderOrder
const customCols: CustomCols<IData> = {
    Other: {
        title: '正确率',
        render: record => <div>{record.Id}</div>
    }
};

const data: IData[] = [...];

// 泛型组件 TypeScript 2.9 之后支持的写法
// 需在这里写明泛型
<XTable<IData>
    data={data}
    columns={columns}
    customCols={customCols}
    rowKey={'Id'} // rowKey会自动提示 keyof IData
/>

目前存在的问题

  • rowSelection 中,selectedRowKeys 没有类型提示

  • column 不支持嵌套,即 children 中的 column 没有类型提示

  • 原本的 dataIndex:是列数据在数据项中对应的 key,支持 a.b.c、a[0].b.c[1] 的嵌套写法,我们的强类型 Table 不支持

  • 有一定重写成本

进一步思考

动手做了个小实验,发现 ​Ant Design​ 中有三个涉及到数据的组件已经支持了泛型:

Select

select.d.ts

declare class Select<ValueType extends SelectValue = SelectValue> extends React.Component<SelectProps<ValueType>> {...}

export interface SelectProps<VT> extends ... {...}

declare type RawValue = string | number;
export interface LabeledValue {
    key?: string;
    value: RawValue;
    label: React.ReactNode;
}
export declare type SelectValue = RawValue | RawValue[] | LabeledValue | LabeledValue[];

业务:课次多选器

<Select<string[]>
    mode="multiple"
    maxTagCount={4}
    value={currKeciList}
    onChange={handleSelectKeci}
>
    {keciList.map(keci => (
        <Select.Option
            key={keci.keciId}
            value={keci.keciId}
        >
            {keci.keciName}
        </Select.Option>
    ))}
</Select>

List

list.d.ts

declare function List<T>({ ... }: ListProps<T>): JSX.Element;
export interface ListProps<T> {
    dataSource?: T[];
    rowKey?: ((item: T) => string) | string;
    renderItem?: (item: T, index: number) => React.ReactNode;
    ......
}

Usage

<List<string>
  header={<div>Header</div>}
  footer={<div>Footer</div>}
  dataSource={data}
  renderItem={item => <List.Item>{item}</List.Item>}
/>

Table

table.d.ts

declare function Table<RecordType extends object = any>(props: TableProps<RecordType>): JSX.Element;

export interface TableProps<RecordType> extends RcTableProps<RecordType> {
    dataSource?: RecordType[];;
    columns?: ColumnsType<RecordType>;
    rowSelection?: TableRowSelection<RecordType>;
    ......
}

export declare type ColumnsType<RecordType = unknown> = (ColumnGroupType<RecordType> | ColumnType<RecordType>)[];

摘自:https://ant.design/components/table-cn/

import { Table } from 'antd';
import { ColumnsType } from 'antd/es/table';

interface User {
  key: number;
  name: string;
}

const columns: ColumnsType<User>[] = [{
  key: 'name',
  title: 'Name',
  dataIndex: 'name',
}];

const data: User[] = [{
  key: 0,
  name: 'Jack',
}];

class UserTable extends Table<User> {}
<UserTable columns={columns} dataSource={data} />

// 使用 JSX 风格的 API
class NameColumn extends Table.Column<User> {}

<UserTable dataSource={data}>
  <NameColumn key="name" title="Name" dataIndex="name" />
</UserTable>

// TypeScript 2.9 之后也可以这样写
// https://www.typescriptlang.org/docs/handbook/release-notes/typescript-2-9.html#generic-type-arguments-in-jsx-elements
<Table<User> columns={columns} dataSource={data} />
<Table<User> dataSource={data}>
  <Table.Column<User> key="name" title="Name" dataIndex="name" />
</Table>

暂时没有发现别的组件支持泛型,也的确只有数据展示型的组件需要这样设计。

在生产开发中提取通用的组件时,可以考虑设计为泛型组件:在更具通用性的同时避免误用,借助typescript为写代码提高效率和质量。

最后更新于

这有帮助吗?