在前端專案裡,表單開發很常見,但有幾個痛點:
我們今天要做的「型別驅動表單生成器」目標是:
假設我們有一個使用者資料型別:
ts
CopyEdit
type User = {
id: string;
name: string;
email: string;
age: number;
isAdmin: boolean;
};
我們希望每個欄位有:
可以用 映射型別(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 };
};
有時候表單 UI 要用 field-${欄位名}
當 id,我們可以:
ts
CopyEdit
type FieldId<T> = `${Extract<keyof T, string>}-field`;
// "id-field" | "name-field" | "email-field" | ...
假設我們要讓部分欄位變成可選:
ts
CopyEdit
type OptionalFields<T, Keys extends keyof T> =
Omit<T, Keys> & Partial<Pick<T, Keys>>;
用法:
ts
CopyEdit
type UserFormWithOptionalEmail = OptionalFields<User, "email">;
我們希望用一個函式來生成表單設定,並且 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 },
});
這裡如果欄位名錯、型別不對,編譯期就會報錯。
有時候我們只需要表單值,不需要 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 }
如果要將表單資料送到 API,可以直接使用:
ts
CopyEdit
type CreateUserApiInput = Omit<User, "id">;
type UpdateUserApiInput = Partial<User>;
搭配表單生成器:
ts
CopyEdit
function submitForm(data: CreateUserApiInput) {
// 呼叫 API...
}
錯誤 1:欄位名沒有跟資料型別同步
→ 解法:用 keyof T
遍歷,讓 TS 自動生成
錯誤 2:多層巢狀型別難以維護
→ 解法:分成多個小型別(FieldConfig、FormValues)
錯誤 3:過度泛型化
→ 解法:只在需要變動的地方用泛型,避免型別過複雜