用強資料型別保護你的表單資料-基於antd表單的型別約束

2023-11-15 18:00:44

概述

介面資料型別與表單提交資料型別,在大多數情況下,大部分屬性的型別是相同的,但很少能做到完全統一。

我在之前的工作中經常為了方便,直接將介面資料型別複用為表單內資料型別,在遇到屬性型別不一致的情況時會使用any強制忽略型別錯誤。

後來經過自省與思考,這種工作模式會引起各種隱藏bug,一定有更好的工程解決方案。

我的答案就是:為表單提交資料單獨定義型別!

型別解說

介面定義的請求體型別

請求資料型別

type RequestBody = {
   name?: string
   count?: number
   groupIds?: number[]
   startDate?: string // YYYY-MM-DD
}


表單提交資料型別定義

type FormValue = {
  name?: string
  count?: number
  groupIds?: string
  startDate?: Moment
}


有了該型別,我們可以方便的將該型別使用在表單範例上

const [form] = Form.useForm< FormValue >()


型別複用優化

如果表單的資料型別和介面提交的資料型別完全一致,當然可以共用一個,但現實世界這種情況幾乎沒有。

大多數情況是可以複用一些介面的屬性到表單的資料型別中,例如上面的兩個資料結構,其中 name、id 屬性是相同的,則FormValue 可以優化為

type FormValue = Pick< RequestBody, 'name' | 'count' > {
  groupIds?: string
  startDate?: Moment
}


Form.Item 限定 name 優化

應用此時表單元件的name約束就應為我們自定義的表單資料型別FormValue,定義約束元件

const FormItem = Form.Item as React.FC<
  Omit< FormItemProps, 'name' > & {
    name: keyof FormValue
  }
>


應用該約束元件

< FormItem label="名稱" name="name" > ...


資料轉換

在表單的onFinish提交過程中,需要一個將FormValue(表單資料)轉換為RequestBody(提交資料)的函數,型別定義如下:

const formValueToRquestBody = (values: FormValue): RequestBody => {
  return {
    name: values.name,
    id: values.id,
    groupIds: values.groupIds.split(',').map(n => Number(n)),
    startDate: values.startDate?.format('YYYY-MM-DD'),
  }
}


複雜表單型別

複雜表單有些表單資料並非一層的key => value,而是多層樹狀或陣列結構。

例如:提交資料結構

type FormValue = {
  name: string
  rule: {
    min: number
    max: number
  }
}


表單中關於rule 的寫法為:

< Form.Item name={['rule', 'min']}>


這種情況下,name不再是簡單的字串,應該如何用型別約束?

如果可以我希望使用型別工具,相容多層表單資料結構,但一直沒成功。

我目前的方法是,為該表單層級安排一個專用型別,該方法會有些寫的麻煩,但勝在能準確的定義好型別。

我在採用該方法校驗表單name資料時發現了幾個很難發現的拼寫錯誤,提前制止了測試同學提bug過來。

例如為rule屬性定義單獨的RuleFormItem

import type { FormItemProps } from 'antd'

const RuleFormItem = Form.Item as React.FC<
  Omit< FormItemProps, 'name'> & {
    name: ['rule', keyof FormValue['rule']]
  }
>


呼叫時

< RuleFormItem label="min" name={['rule', 'min']}> ...


此時陣列中的 rule 與 min 都能收到型別的保護。

泛型抽象

export type TypedFormItem< T > = React.FC<
  Omit< FormItemProps, 'name' > & {
    name: T
  }
>


應用泛型

const RuleFormItem = Form.Item as TypedFormItem< keyof FormValue >