iT邦幫忙

2025 iThome 鐵人賽

DAY 27
0

1) 引言:為什麼要「型別驅動」?

在前端專案裡,表單開發很常見,但有幾個痛點:

  1. 欄位定義重複:資料模型、表單 UI、驗證規則常常要寫三遍
  2. 型別不同步:改了資料型別,但表單程式沒更新
  3. 易出錯:手動維護的欄位名稱容易 typo

我們今天要做的「型別驅動表單生成器」目標是:

  • 欄位型別一次定義,多處共用
  • 表單的欄位結構、Label、驗證規則全由型別推導
  • 有型別提示,不會輸入錯的 key

2) 建立資料模型

假設我們有一個使用者資料型別:

ts
CopyEdit
type User = {
  id: string;
  name: string;
  email: string;
  age: number;
  isAdmin: boolean;
};


3) 定義表單欄位設定型別

我們希望每個欄位有:

  • label(顯示名稱)
  • value(欄位值,跟資料型別一致)
  • required(是否必填)

可以用 映射型別(Day 25)

ts
CopyEdit
type FormFieldConfig<T> = {
  [K in keyof T]: {
    label: string;
    value: T[K];
    required: boolean;
  };
};

這樣 FormFieldConfig<User> 會長這樣:

ts
CopyEdit
type UserFormConfig = {
  id: { label: string; value: string; required: boolean };
  name: { label: string; value: string; required: boolean };
  email: { label: string; value: string; required: boolean };
  age: { label: string; value: number; required: boolean };
  isAdmin: { label: string; value: boolean; required: boolean };
};


4) 自動生成欄位名稱(Template Literal Types)

有時候表單 UI 要用 field-${欄位名} 當 id,我們可以:

ts
CopyEdit
type FieldId<T> = `${Extract<keyof T, string>}-field`;
// "id-field" | "name-field" | "email-field" | ...


5) 加上可選欄位(條件型別)

假設我們要讓部分欄位變成可選:

ts
CopyEdit
type OptionalFields<T, Keys extends keyof T> =
  Omit<T, Keys> & Partial<Pick<T, Keys>>;

用法:

ts
CopyEdit
type UserFormWithOptionalEmail = OptionalFields<User, "email">;


6) 表單生成器函式(泛型 + 條件型別)

我們希望用一個函式來生成表單設定,並且 TS 能推導型別:

ts
CopyEdit
function createFormConfig<T>(config: FormFieldConfig<T>): FormFieldConfig<T> {
  return config;
}

用法:

ts
CopyEdit
const userForm = createFormConfig<User>({
  id: { label: "User ID", value: "", required: true },
  name: { label: "Full Name", value: "", required: true },
  email: { label: "Email", value: "", required: true },
  age: { label: "Age", value: 0, required: false },
  isAdmin: { label: "Admin", value: false, required: false },
});

這裡如果欄位名錯、型別不對,編譯期就會報錯。


7) 實戰應用:根據資料生成表單值型別

有時候我們只需要表單值,不需要 label:

ts
CopyEdit
type FormValues<T> = {
  [K in keyof T]: T[K];
};

type UserFormValues = FormValues<User>;
// { id: string; name: string; email: string; age: number; isAdmin: boolean }


8) 加上 API 輸入輸出型別

如果要將表單資料送到 API,可以直接使用:

ts
CopyEdit
type CreateUserApiInput = Omit<User, "id">;
type UpdateUserApiInput = Partial<User>;

搭配表單生成器:

ts
CopyEdit
function submitForm(data: CreateUserApiInput) {
  // 呼叫 API...
}


9) 常見錯誤與陷阱

錯誤 1:欄位名沒有跟資料型別同步

→ 解法:用 keyof T 遍歷,讓 TS 自動生成

錯誤 2:多層巢狀型別難以維護

→ 解法:分成多個小型別(FieldConfig、FormValues)

錯誤 3:過度泛型化

→ 解法:只在需要變動的地方用泛型,避免型別過複雜


10) 心法

  1. 資料模型是單一真相來源(Single Source of Truth)
    • 從資料型別推導表單結構,避免重複定義
  2. 型別系統就是你的表單驗證第一道關卡
    • 錯誤輸入直接在編譯期擋下
  3. 與 API 型別對齊
    • 表單型別應直接對應 API 輸入輸出

上一篇
Day 26|Template Literal Types:型別也能玩字串拼接
系列文
我與型別的 30 天約定:TypeScript 入坑實錄27
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言