通用 UseForm 钩子
Generic UseForm Hook
我正在尝试使用泛型和 Typescript 创建一个 useForm 挂钩。
我希望消费者能够输入一个界面和初始状态,然后让挂钩动态创建一个带有添加验证字段的新状态。
例如用户传入:
const {formState, handleSubmit, updateField} = useForm<{
email: string;
}>({
email: "",
});
在幕后,钩子会创建这样的状态:
{
email: {
value: "",
validationMessage: "",
validationType: "none" // as opposed to "success" or "error"
}
}
我正在尝试通过 reduce 实现这一点,代码如下:
interface FormField {
value: string,
validationType: ValidationProperty,
validationMessage: string
}
function isInInitialState<T>(val: string | keyof T, vals: T): val is keyof T {
return typeof val === "string" && Object.keys(vals).includes(val)
}
function useForm<T>(initialFormState: T) {
const [formState, setFormState] = useState<Record<keyof T, FormField>>(Object.keys(initialFormState).reduce<Record<keyof T, FormField>>((previous, current) => {
return {
...previous,
[current]: {
value: isInInitialState(current, initialFormState) ? previous[current] : "",
validationType: "none",
validationMessage: ""
}
}
}, {}));
...rest of hook.
Typescript 的问题是我的空对象的起始值与 T 的形状不同,因此编译器会抛出错误。同样,如果我使用与 initialFormState
相同的值填充起始值,我也会遇到同样的问题。
简而言之,我如何告诉 Typescript 我肯定会以 Record<keyof T, FormField>>
结束,所以它不需要担心起始值?
我认为您可以使用类型断言来解决您的问题。
有时 typescript 无法正确推断类型,当您调用 reduce 函数时就会发生这种情况。因此,您可以告诉打字稿您的常数的预期 return 是什么。
type ValidationProperty = 'none' | 'other'
interface FormField {
value: string
validationType: ValidationProperty
validationMessage: string
}
type FormState<T> = Record<keyof T, FormField>
function useForm<T>(initialFormState: T) {
const keys = Object.keys(initialFormState)
const defaultFormState = keys.reduce(
(acc, key) => ({
...acc,
[key]: {
value: initialFormState[key],
validationType: 'none',
validationMessage: ''
}
}),
{}
) as FormState<T> // Type assertion here
const [formState, setFormState] = useState<FormState<T>>(defaultFormState)
return [formState]
}
const MyComponent = () => {
const [form] = useForm({ email: 'string' })
// form has the type FormState<{email: string}>
return <div>Hello</div>
}
使用 type assertion will be helpful in your case. The reason this will be necessary is that the return type of Object.keys()
is always string[]
in TypeScript. This is because TS is structurally-typed,因此对象可以具有超出您在类型中定义的属性的额外属性。
在这种情况下提取转换函数也很有用:
import {useState} from 'react';
type ValidationType = 'error' | 'none' | 'success';
type FormField = {
validationMessage: string;
validationType: ValidationType;
value: string;
};
function transformObject <T extends Record<string, string>>(o: T): Record<keyof T, FormField> {
return Object.keys(o).reduce((previous, key) => ({
...previous,
[key]: {
value: o[key as keyof T],
validationType: 'none',
validationMessage: '',
}
}), {} as Record<keyof T, FormField>);
}
function useForm <T extends Record<string, string>>(initialFormState: T) {
const [formState, setFormState] = useState(transformObject(initialFormState));
// rest of hook...
}
transformObject({email: 999}); // Desirable compiler error
// ~~~~~
// Type 'number' is not assignable to type 'string'.(2322)
const transformed = transformObject({email: 'recipient@name.tld'}); // ok
console.log(transformed); // Result:
// {
// "email": {
// "value": "recipient@name.tld",
// "validationType": "none",
// "validationMessage": ""
// }
// }
我正在尝试使用泛型和 Typescript 创建一个 useForm 挂钩。 我希望消费者能够输入一个界面和初始状态,然后让挂钩动态创建一个带有添加验证字段的新状态。
例如用户传入:
const {formState, handleSubmit, updateField} = useForm<{
email: string;
}>({
email: "",
});
在幕后,钩子会创建这样的状态:
{
email: {
value: "",
validationMessage: "",
validationType: "none" // as opposed to "success" or "error"
}
}
我正在尝试通过 reduce 实现这一点,代码如下:
interface FormField {
value: string,
validationType: ValidationProperty,
validationMessage: string
}
function isInInitialState<T>(val: string | keyof T, vals: T): val is keyof T {
return typeof val === "string" && Object.keys(vals).includes(val)
}
function useForm<T>(initialFormState: T) {
const [formState, setFormState] = useState<Record<keyof T, FormField>>(Object.keys(initialFormState).reduce<Record<keyof T, FormField>>((previous, current) => {
return {
...previous,
[current]: {
value: isInInitialState(current, initialFormState) ? previous[current] : "",
validationType: "none",
validationMessage: ""
}
}
}, {}));
...rest of hook.
Typescript 的问题是我的空对象的起始值与 T 的形状不同,因此编译器会抛出错误。同样,如果我使用与 initialFormState
相同的值填充起始值,我也会遇到同样的问题。
简而言之,我如何告诉 Typescript 我肯定会以 Record<keyof T, FormField>>
结束,所以它不需要担心起始值?
我认为您可以使用类型断言来解决您的问题。
有时 typescript 无法正确推断类型,当您调用 reduce 函数时就会发生这种情况。因此,您可以告诉打字稿您的常数的预期 return 是什么。
type ValidationProperty = 'none' | 'other'
interface FormField {
value: string
validationType: ValidationProperty
validationMessage: string
}
type FormState<T> = Record<keyof T, FormField>
function useForm<T>(initialFormState: T) {
const keys = Object.keys(initialFormState)
const defaultFormState = keys.reduce(
(acc, key) => ({
...acc,
[key]: {
value: initialFormState[key],
validationType: 'none',
validationMessage: ''
}
}),
{}
) as FormState<T> // Type assertion here
const [formState, setFormState] = useState<FormState<T>>(defaultFormState)
return [formState]
}
const MyComponent = () => {
const [form] = useForm({ email: 'string' })
// form has the type FormState<{email: string}>
return <div>Hello</div>
}
使用 type assertion will be helpful in your case. The reason this will be necessary is that the return type of Object.keys()
is always string[]
in TypeScript. This is because TS is structurally-typed,因此对象可以具有超出您在类型中定义的属性的额外属性。
在这种情况下提取转换函数也很有用:
import {useState} from 'react';
type ValidationType = 'error' | 'none' | 'success';
type FormField = {
validationMessage: string;
validationType: ValidationType;
value: string;
};
function transformObject <T extends Record<string, string>>(o: T): Record<keyof T, FormField> {
return Object.keys(o).reduce((previous, key) => ({
...previous,
[key]: {
value: o[key as keyof T],
validationType: 'none',
validationMessage: '',
}
}), {} as Record<keyof T, FormField>);
}
function useForm <T extends Record<string, string>>(initialFormState: T) {
const [formState, setFormState] = useState(transformObject(initialFormState));
// rest of hook...
}
transformObject({email: 999}); // Desirable compiler error
// ~~~~~
// Type 'number' is not assignable to type 'string'.(2322)
const transformed = transformObject({email: 'recipient@name.tld'}); // ok
console.log(transformed); // Result:
// {
// "email": {
// "value": "recipient@name.tld",
// "validationType": "none",
// "validationMessage": ""
// }
// }