提效保质:使用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>)[];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为写代码提高效率和质量。
最后更新于
这有帮助吗?