TypeScript-Class(3)
类泛型
看一个简单的抽屉类
// 这里有一个抽屉,它可以容纳任何种类的物体,接收一个类型
class Drawer<ClothingType> {
contents: ClothingType[] = []
add (object: ClothingType) {
this.contents.push(object)
return this
}
removeLast () {
this.contents.pop()
return this
}
}
// 定义一个🧦类型
interface Sock {
color: string
type: 'short' | 'middle' | 'long'
}
// 定义一个👕类型
interface TShirt {
size: 'xxs' | 'm' | 'l' | 'xxl'
}
// 创建一个新抽屉时传入Sock类型来创建仅用于🧦的抽屉:
const sockDrawer = new Drawer<Sock>()
// 添加🧦 & 移除最后一双🧦
sockDrawer.add({ color: 'white', type: 'short' }).removeLast()
// 传入TShirt类型来创建仅用于👕的抽屉
const tshirtDrawer = new Drawer<TShirt>()
// 添加👕
tshirtDrawer.add({ size: 'xxl' })
// 如果东西摆放比较随意,可以创建一个抽屉来混合🧦和👕
const mixedDrawer = new Drawer<Sock | TShirt>()
mixedDrawer.add({ color: 'white', type: 'short' }).add({ size: 'xxl' })
console.warn(mixedDrawer) // { "contents": [ { "color": "white", "type": "short" }, { "size": "xxl" } ] }
多态的This类型
- 介绍:多态的
this
类型表示的是某个包含类或接口的 子类型。 这被称做_F_``-bounded
多态性。 它能很容易的表现连贯接口间的继承
// 定义一个可以扔掉所有东西的抽屉
class CThrowAwayAllDrawer<ClothingType> extends CDrawer<ClothingType> {
throwAwayAll () {
this.contents = []
return this
}
}
const throwAwayAllDrawer = new CThrowAwayAllDrawer<Sock>()
throwAwayAllDrawer.add({ color: 'white', type: 'short' }).throwAwayAll()
由于这个类和父类的方法均返回了this
this
类型,所以子类可以直接链式调用之前的方法
如果返回没有
this
类型,add
将会返回CDrawer
,它并没有throwAwayAll
方法。 如果返回this
类型,add
将会返回CThrowAwayAllDrawer
业务场景
1. 表单模板组件的ts编写
- 目的:实现根据确定的组件类型来提示当前组件的相关属性
将会涉及知识点: 可辨识联合(Discriminated Unions) keyof T T[keyof T] antd组件类型引用 '&' && '|' ...
1.1 表单配置基础类型
- 功能:可根据component的类型来渲染不同组件
import { ReactNode } from 'react'
// 第三方类型引用
import { FormItemProps } from 'antd/lib/form/FormItem.d'
import { ColProps } from 'antd/lib/grid/col.d'
export type ComponentType = 'Checkbox' | 'Radio' | 'TreeSelect' | 'Cascader' | 'DatePicker' | 'InputNumber' | 'RangePicker' | 'Select' | 'TextArea' | 'custom' | 'InputGroup' | 'subFormItem' | 'Upload'
export class IGenerateFormItemParams {
'config': {
label?: ReactNode,
key?: string | number | string[],
component?: ComponentType,
componentProps?: any,
optionsProps?(data: Record<string, any>): Record<string, any>,
formItemProps?: Omit<FormItemProps, 'children'>,
children?: ReactNode,
subFormConfig?: IGenerateFormItemParams['config'][],
subComponent?: string,
display?: boolean,
colProps?: ColProps,
}
...
}
- 已实现:大部分属性的提示
- 未实现:
- componentProps、subComponent、children 的属性提示
- 属性的正确归属(e.g: optionsProps只属于component为Select的组件等)
1.2 实现componentProps、subComponent的属性提示及属性的正确归属
1.2.1 定义一堆约束不同组件类型的Class
// 基础类型
export class CBasicComponent<T extends string, CP extends Record<string, any>> {
'component': T
'componentProps'?: CP
}
// 基础类型包含optionsProps
export class CBasicComponentWithOptionsProps<T extends string, CP extends Record<string, any>, OPR extends Record<string, any> = Record<string, any>> extends CBasicComponent<T, CP> {
optionsProps?(data: Record<string, any>): OPR
}
// 基础类型包含subComponent
export class CBasicComponentWithSubComponent<T extends string, CP extends Record<string, any>, SBC extends string> extends CBasicComponent<T, CP> {
'subComponent'?: SBC
}
// 基础类型包含optionsProps & subComponent
export class CBasicComponentWithSubComponentAndOptionsProps<T extends string, CP extends Record<string, any>, OPR extends Record<string, any> = Record<string, any>, SBC extends string = never> extends CBasicComponent<T, CP> {
optionsProps?(data: Record<string, any>): OPR
'subComponent'?: SBC
}
上面的方式比较清晰功能也能实现,就是比较繁琐,可能有其他更好的方案
1.2.2 添加对应组件类型
import { CheckboxGroupProps, CheckboxOptionType } from 'antd/lib/checkbox/index.d'
import { RadioGroupProps } from 'antd/lib/radio/interface'
// 基于可辨识联合的特性实现
export type CCheckBox = CBasicComponentWithOptionsProps<'Checkbox', CheckboxGroupProps, CheckboxOptionType>
export type CRadio = CBasicComponentWithOptionsProps<'Radio', RadioGroupProps, CheckboxOptionType>
...
1.2.3 扩展基础表单类型
export class IGenerateFormItemParams {
'config': {
...
- component?: ComponentType,
- componentProps?: any,
...
+ } & (CCheckBox | CRadio | { component?: null }) // 兼容不传component的情况
}
比较恶心的点是组件类型的添加,上面加一个下面也要加一个,增加了维护成本(虽然组件基本是稳定的,基本不会添加,但是优化是无止尽的[手动狗头]),下面提供一个优化方案
1.2.4 优化类型的维护
利用T[key of]的特性
// 添加映射列表, 后面只需要关注这里
+ export class IComponentProps {
+ 'Checkbox': CCheckBox
+ 'Radio': CRadio
+ }
export class IGenerateFormItemParams {
'config': {
...
- component?: ComponentType,
- componentProps?: any,
...
+ } & (IComponentProps[keyof IComponentProps] | { component?: null }) // 兼容不传component的情况
}
现在基本上已经实现了所有功能,可能后续还有坑吧,问题不大
1.3 测试结果
const testResult: IGenerateFormItemParams['config'][] = [{
component: 'Checkbox',
label: '提测checklist',
key: 'testSelf',
componentProps: {
options: [
{ value: 1, label: '冒烟自测' },
],
},
colProps: {
span: 12,
},
formItemProps: {
labelCol: { span: 8 },
wrapperCol: { span: 16 },
},
},
{
component: 'Radio',
key: 'review',
componentProps: {
options: [
{ value: 1, label: 'Code Review' },
],
},
colProps: {
span: 6,
},
}, {
label: '12',
}]
console.warn(testResult)
你以为到此就结束了嘛[手动狗头]
1.4 优化永无止境之另一种思路实现组件类型定义
定义可扩展类型的类型
type TBasic<T extends string, CP extends Record<string, any>, EP extends Record<string, any>> = ({
'component': T,
'componentProps'?: CP,
} & EP)
// e.g:
const test: TBasic<'Checkbox', CheckboxGroupProps, { optionsProps?(data: Record<string, any>): Record<string, any> }>
typeof test => {
component: 'Checkbox',
componentProps?: CheckboxGroupProps,
optionsProps?(data: Record<string, any>): Record<string, any>
}
1.5 最终代码
import { ReactNode } from 'react'
import { FormItemProps } from 'antd/lib/form/FormItem.d'
import { ColProps } from 'antd/lib/grid/col.d'
import { CheckboxGroupProps } from 'antd/lib/checkbox/index.d'
import { RadioGroupProps } from 'antd/lib/radio/interface'
import { TreeSelectProps } from 'antd/lib/tree-select'
import { CascaderProps } from 'antd/lib/cascader'
import { DatePickerProps } from 'antd/lib/date-picker'
import { InputNumberProps } from 'antd/lib/input-number'
import { RangePickerBaseProps } from 'antd/lib/date-picker/generatePicker'
import { SelectProps } from 'antd/lib/select'
import { TextAreaProps, InputProps, GroupProps } from 'antd/lib/input'
type TBasic<T extends string, CP extends Record<string, any>, EP extends Record<string, any> = Record<string, any>> = ({
component: T,
componentProps?: CP,
} & EP)
type TSubForm<T extends string> = ({
component: T
subFormConfig: FormCustomItem[]
})
export interface IComponentProps {
'Checkbox': TBasic<'Checkbox', CheckboxGroupProps & { options: IBaseOptions[] }>
'Radio': TBasic<'Radio', RadioGroupProps, { subComponent?: 'Group' | 'Button' }>
'TreeSelect': TBasic<'TreeSelect', TreeSelectProps<import('rc-tree-select/lib/interface').DefaultValueType>>
'Cascader': TBasic<'Cascader', CascaderProps>
'DatePicker': TBasic<'DatePicker', DatePickerProps & {submitFormat?: string }>
'RangePicker': TBasic<'RangePicker', RangePickerBaseProps<any>>
'InputNumber': TBasic<'InputNumber', InputNumberProps>
'Select': TBasic<'Select', SelectProps<any>, { optionsProps?(data: Record<string, any>): Record<string, any> }>
'TextArea': TBasic<'TextArea', TextAreaProps>
'Upload': TBasic<'Upload', import('@/components/upload-type-file/index').IParams>
'custom': {
component: 'custom',
children: ReactNode
}
'InputGroup': TSubForm<'InputGroup'> & { componentProps: GroupProps }
'subFormItem': TSubForm<'subFormItem'>
}
// TODO: 这个方案少些代码,但是感觉看起来不直观
// export type IComponentProps =
// | TBasic<'Checkbox', CheckboxGroupProps, { optionsProps?(data: Record<string, any>): Record<string, any> }>
// | TBasic<'Radio', RadioGroupProps, { subComponent?: 'Group' | 'Button' }>
// | { component: 'custom', children: ReactNode }
// | TSubForm<'InputGroup'>
// | TSubForm<'subFormItem'>
// | { component?: unknown }
/**
* 自定义表单组件的子项
*/
export type FormCustomItem = {
label?: ReactNode,
key?: string | number | string[],
formItemProps?: Omit<FormItemProps, 'children'>,
display?: boolean,
colProps?: ColProps,
} & (IComponentProps[keyof IComponentProps] | { component?: null, componentProps?: InputProps }) // 兼容不传component的情况,
/**
* 自定义表单组件
*/
export interface IGenerateFormItemParams {
config: FormCustomItem,
formItemLayout?: {
labelCol?: ColProps;
wrapperCol?: ColProps;
}
index?: number
}
总结
ts是一个随便写写也能运行,但是写好不容易的语言(毕竟是js的超集),在实现这个功能的时候提取现有知识 + 疯狂google,但是并没有找到实现方案,后来凭借脑海中仅有的那么点印象自己手动尝试方案,遇到一些坑点(轻描淡写的说遇到一些,实际上!!!!!),然后优化方案,好在结果是满意的。
优化永无止境,继续加油!
参考: