TypeScript-Class(3)

575 阅读5分钟

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()

由于这个类和父类的方法均返回了thisthis类型,所以子类可以直接链式调用之前的方法

如果返回没有 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,但是并没有找到实现方案,后来凭借脑海中仅有的那么点印象自己手动尝试方案,遇到一些坑点(轻描淡写的说遇到一些,实际上!!!!!),然后优化方案,好在结果是满意的。

优化永无止境,继续加油!

参考: